summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj17
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookClient.cs28
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookGraph.cs54
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth1/GoogleConsumer.cs (renamed from samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs)0
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth1/TwitterConsumer.cs (renamed from samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs)12
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth1/YammerConsumer.cs (renamed from samples/DotNetOpenAuth.ApplicationBlock/YammerConsumer.cs)0
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Facebook/FacebookClient.cs105
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Facebook/FacebookGraph.cs305
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Google/GoogleClient.cs107
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Google/GoogleGraph.cs152
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth2/IOAuth2Graph.cs34
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth2/WindowsLive/WindowsLiveClient.cs181
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/OAuth2/WindowsLive/WindowsLiveGraph.cs299
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/WindowsLiveClient.cs51
-rw-r--r--samples/DotNetOpenAuth.ApplicationBlock/WindowsLiveGraph.cs60
-rw-r--r--samples/OAuthClient/Default.aspx5
-rw-r--r--samples/OAuthClient/Facebook.aspx.cs38
-rw-r--r--samples/OAuthClient/Google.aspx16
-rw-r--r--samples/OAuthClient/Google.aspx.cs37
-rw-r--r--samples/OAuthClient/Google.aspx.designer.cs33
-rw-r--r--samples/OAuthClient/OAuthClient.csproj15
-rw-r--r--samples/OAuthClient/Web.config3
-rw-r--r--samples/OAuthClient/WindowsLive.aspx.cs45
-rw-r--r--samples/OAuthClient/packages.config4
24 files changed, 1356 insertions, 245 deletions
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj
index 6c2ad7a..50b8914 100644
--- a/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj
+++ b/samples/DotNetOpenAuth.ApplicationBlock/DotNetOpenAuth.ApplicationBlock.csproj
@@ -94,20 +94,23 @@
<Compile Include="CustomExtensions\Acme.cs" />
<Compile Include="CustomExtensions\AcmeRequest.cs" />
<Compile Include="CustomExtensions\AcmeResponse.cs" />
- <Compile Include="Facebook\FacebookClient.cs" />
- <Compile Include="Facebook\FacebookGraph.cs" />
+ <Compile Include="OAuth2\Facebook\FacebookClient.cs" />
+ <Compile Include="OAuth2\Facebook\FacebookGraph.cs" />
<Compile Include="CustomExtensions\UIRequestAtRelyingPartyFactory.cs" />
- <Compile Include="GoogleConsumer.cs" />
+ <Compile Include="OAuth1\GoogleConsumer.cs" />
<Compile Include="InMemoryClientAuthorizationTracker.cs" />
<Compile Include="InMemoryTokenManager.cs">
<SubType>Code</SubType>
</Compile>
+ <Compile Include="OAuth2\Google\GoogleClient.cs" />
+ <Compile Include="OAuth2\Google\GoogleGraph.cs" />
+ <Compile Include="OAuth2\IOAuth2Graph.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
- <Compile Include="TwitterConsumer.cs" />
+ <Compile Include="OAuth1\TwitterConsumer.cs" />
<Compile Include="Util.cs" />
- <Compile Include="WindowsLiveClient.cs" />
- <Compile Include="WindowsLiveGraph.cs" />
- <Compile Include="YammerConsumer.cs" />
+ <Compile Include="OAuth2\WindowsLive\WindowsLiveClient.cs" />
+ <Compile Include="OAuth2\WindowsLive\WindowsLiveGraph.cs" />
+ <Compile Include="OAuth1\YammerConsumer.cs" />
<Compile Include="YubikeyRelyingParty.cs" />
</ItemGroup>
<ItemGroup>
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookClient.cs b/samples/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookClient.cs
deleted file mode 100644
index a24e5b3..0000000
--- a/samples/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookClient.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="FacebookClient.cs" company="Outercurve Foundation">
-// Copyright (c) Outercurve Foundation. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOpenAuth.ApplicationBlock {
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Web;
- using DotNetOpenAuth.Messaging;
- using DotNetOpenAuth.OAuth2;
-
- public class FacebookClient : WebServerClient {
- private static readonly AuthorizationServerDescription FacebookDescription = new AuthorizationServerDescription {
- TokenEndpoint = new Uri("https://graph.facebook.com/oauth/access_token"),
- AuthorizationEndpoint = new Uri("https://graph.facebook.com/oauth/authorize"),
- };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="FacebookClient"/> class.
- /// </summary>
- public FacebookClient() : base(FacebookDescription) {
- }
- }
-}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookGraph.cs b/samples/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookGraph.cs
deleted file mode 100644
index 909ae9a..0000000
--- a/samples/DotNetOpenAuth.ApplicationBlock/Facebook/FacebookGraph.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="FacebookGraph.cs" company="Outercurve Foundation">
-// Copyright (c) Outercurve Foundation. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOpenAuth.ApplicationBlock.Facebook {
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Runtime.Serialization;
- using System.Runtime.Serialization.Json;
- using System.Text;
-
- [DataContract]
- public class FacebookGraph {
- private static DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(FacebookGraph));
-
- [DataMember(Name = "id")]
- public long Id { get; set; }
-
- [DataMember(Name = "name")]
- public string Name { get; set; }
-
- [DataMember(Name = "first_name")]
- public string FirstName { get; set; }
-
- [DataMember(Name = "last_name")]
- public string LastName { get; set; }
-
- [DataMember(Name = "link")]
- public Uri Link { get; set; }
-
- [DataMember(Name = "birthday")]
- public string Birthday { get; set; }
-
- public static FacebookGraph Deserialize(string json) {
- if (string.IsNullOrEmpty(json)) {
- throw new ArgumentNullException("json");
- }
-
- return Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(json)));
- }
-
- public static FacebookGraph Deserialize(Stream jsonStream) {
- if (jsonStream == null) {
- throw new ArgumentNullException("jsonStream");
- }
-
- return (FacebookGraph)jsonSerializer.ReadObject(jsonStream);
- }
- }
-}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth1/GoogleConsumer.cs
index 1bdb04d..1bdb04d 100644
--- a/samples/DotNetOpenAuth.ApplicationBlock/GoogleConsumer.cs
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth1/GoogleConsumer.cs
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth1/TwitterConsumer.cs
index e9a27a4..013e66b 100644
--- a/samples/DotNetOpenAuth.ApplicationBlock/TwitterConsumer.cs
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth1/TwitterConsumer.cs
@@ -28,9 +28,9 @@ namespace DotNetOpenAuth.ApplicationBlock {
/// a user's private Twitter data.
/// </summary>
public static readonly ServiceProviderDescription ServiceDescription = new ServiceProviderDescription {
- RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
- UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
- AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/access_token", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
+ RequestTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
+ UserAuthorizationEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/authorize", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
+ AccessTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
};
@@ -38,9 +38,9 @@ namespace DotNetOpenAuth.ApplicationBlock {
/// The description of Twitter's OAuth protocol URIs for use with their "Sign in with Twitter" feature.
/// </summary>
public static readonly ServiceProviderDescription SignInWithTwitterServiceDescription = new ServiceProviderDescription {
- RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
- UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
- AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.twitter.com/oauth/access_token", HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
+ RequestTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/request_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
+ UserAuthorizationEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/authenticate", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
+ AccessTokenEndpoint = new MessageReceivingEndpoint("http://twitter.com/oauth/access_token", HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest),
TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
};
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/YammerConsumer.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth1/YammerConsumer.cs
index bbeb861..bbeb861 100644
--- a/samples/DotNetOpenAuth.ApplicationBlock/YammerConsumer.cs
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth1/YammerConsumer.cs
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Facebook/FacebookClient.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Facebook/FacebookClient.cs
new file mode 100644
index 0000000..b8717f7
--- /dev/null
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Facebook/FacebookClient.cs
@@ -0,0 +1,105 @@
+//-----------------------------------------------------------------------
+// <copyright file="FacebookClient.cs" company="Outercurve Foundation">
+// Copyright (c) Outercurve Foundation. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.ApplicationBlock {
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OAuth2;
+
+ public class FacebookClient : WebServerClient {
+ private static readonly AuthorizationServerDescription FacebookDescription = new AuthorizationServerDescription {
+ TokenEndpoint = new Uri("https://graph.facebook.com/oauth/access_token"),
+ AuthorizationEndpoint = new Uri("https://graph.facebook.com/oauth/authorize"),
+ ProtocolVersion = ProtocolVersion.V20
+ };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FacebookClient"/> class.
+ /// </summary>
+ public FacebookClient()
+ : base(FacebookDescription) {
+ }
+
+ public IOAuth2Graph GetGraph(IAuthorizationState authState, string[] fields = null) {
+ if ((authState != null) && (authState.AccessToken != null)) {
+ string fieldsStr = (fields == null) || (fields.Length == 0) ? FacebookGraph.Fields.Defaults : string.Join(",", fields);
+
+ WebRequest request = WebRequest.Create("https://graph.Facebook.com/me?access_token=" + Uri.EscapeDataString(authState.AccessToken) + "&fields=" + fieldsStr);
+ WebResponse response = request.GetResponse();
+
+ if (response != null) {
+ Stream responseStream = response.GetResponseStream();
+
+ if (responseStream != null) {
+ return FacebookGraph.Deserialize(responseStream);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Well-known permissions defined by Facebook.
+ /// </summary>
+ /// <remarks>
+ /// This sample includes just a few permissions. For a complete list of permissions please refer to:
+ /// https://developers.facebook.com/docs/reference/login/
+ /// </remarks>
+ public static class Scopes {
+ #region Email Permissions
+ /// <summary>
+ /// Provides access to the user's primary email address in the email property. Do not spam users. Your use of email must comply both with Facebook policies and with the CAN-SPAM Act.
+ /// </summary>
+ public const string Email = "email";
+ #endregion
+
+ #region Extended Permissions
+ /// <summary>
+ /// Provides access to any friend lists the user created. All user's friends are provided as part of basic data, this extended permission grants access to the lists of friends a user has created, and should only be requested if your application utilizes lists of friends.
+ /// </summary>
+ public const string ReadFriendlists = "read_friendlists";
+
+ /// <summary>
+ /// Provides read access to the Insights data for pages, applications, and domains the user owns.
+ /// </summary>
+ public const string ReadInsights = "read_insights";
+ #endregion
+
+ #region Extended Profile Properties
+ /// <summary>
+ /// Provides access to the "About Me" section of the profile in the about property
+ /// </summary>
+ public const string UserAboutMe = "user_about_me";
+
+ /// <summary>
+ /// Provides access to the user's list of activities as the activities connection
+ /// </summary>
+ public const string UserActivities = "user_activities";
+
+ /// <summary>
+ /// Provides access to the birthday with year as the birthday property. Note that your app may determine if a user is "old enough" to use an app by obtaining the age_range public profile property
+ /// </summary>
+ public const string UserBirthday = "user_birthday";
+ #endregion
+
+ #region Open Graph Permissions
+ #endregion
+
+ #region Page Permissions
+ #endregion
+
+ #region Public Profile and Friend List
+ #endregion
+ }
+ }
+}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Facebook/FacebookGraph.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Facebook/FacebookGraph.cs
new file mode 100644
index 0000000..0c8bb4d
--- /dev/null
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Facebook/FacebookGraph.cs
@@ -0,0 +1,305 @@
+//-----------------------------------------------------------------------
+// <copyright file="FacebookGraph.cs" company="Outercurve Foundation">
+// Copyright (c) Outercurve Foundation. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.ApplicationBlock {
+ using System;
+ using System.Collections.Generic;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using System.Runtime.Serialization;
+ using System.Runtime.Serialization.Json;
+ using System.Text;
+
+ //// Documentation: https://developers.facebook.com/docs/reference/api/user/
+
+ [DataContract]
+ public class FacebookGraph : IOAuth2Graph {
+ private static DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(FacebookGraph));
+
+ /// <summary>
+ /// Gets the user's Facebook ID
+ /// </summary>
+ [DataMember(Name = "id")]
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets the user's full name
+ /// </summary>
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets the user's first name
+ /// </summary>
+ [DataMember(Name = "first_name")]
+ public string FirstName { get; set; }
+
+ /// <summary>
+ /// The user's middle name
+ /// </summary>
+ [DataMember(Name = "middle_name")]
+ public string MiddleName { get; set; }
+
+ /// <summary>
+ /// The user's last name
+ /// </summary>
+ [DataMember(Name = "last_name")]
+ public string LastName { get; set; }
+
+ /// <summary>
+ /// The user's gender: female or male
+ /// </summary>
+ [DataMember(Name = "gender")]
+ public string Gender { get; set; }
+
+ /// <summary>
+ /// The user's locale
+ /// </summary>
+ [DataMember(Name = "locale")]
+ public string Locale { get; set; }
+
+ /// <summary>
+ /// The user's languages
+ /// </summary>
+ [DataMember(Name = "languages")]
+ public FacebookIdName[] Languages { get; set; }
+
+ /// <summary>
+ /// The URL of the profile for the user on Facebook
+ /// </summary>
+ [DataMember(Name = "link")]
+ public Uri Link { get; set; }
+
+ /// <summary>
+ /// The user's Facebook username
+ /// </summary>
+ [DataMember(Name = "username")]
+ public string Username { get; set; }
+
+ // age_range
+
+ // third_party_id
+
+ // installed
+
+ /// <summary>
+ /// The user's timezone offset from UTC
+ /// </summary>
+ [DataMember(Name = "timezone")]
+ public int? Timezone { get; set; }
+
+ /// <summary>
+ /// The last time the user's profile was updated; changes to the languages, link, timezone, verified, interested_in, favorite_athletes, favorite_teams, and video_upload_limits are not not reflected in this value
+ /// string containing an ISO-8601 datetime
+ /// </summary>
+ [DataMember(Name = "updated_time")]
+ public string UpdatedTime { get; set; }
+
+ // verified
+
+ // bio
+
+ /// <summary>
+ /// The user's birthday
+ /// Date string in MM/DD/YYYY format
+ /// </summary>
+ [DataMember(Name = "birthday")]
+ public string Birthday { get; set; }
+
+ [Obsolete]
+ [DataMember(Name = "birthday_date")]
+ public string BirthdayDate { get; set; }
+
+ // cover
+
+ // currency
+
+ // devices
+
+ // education
+
+ /// <summary>
+ /// The proxied or contact email address granted by the user
+ /// </summary>
+ [DataMember(Name = "email")]
+ public string Email { get; set; }
+
+ /// <summary>
+ /// The user's hometown
+ /// </summary>
+ [DataMember(Name = "hometown")]
+ public FacebookIdName Hometown { get; set; }
+
+ /// <summary>
+ /// The genders the user is interested in
+ /// </summary>
+ [DataMember(Name = "interested_in")]
+ public string[] InterestedIn { get; set; }
+
+ /// <summary>
+ /// The user's current city
+ /// </summary>
+ [DataMember(Name = "location")]
+ public FacebookIdName Location { get; set; }
+
+ /// <summary>
+ /// The user's political view
+ /// </summary>
+ [DataMember(Name = "political")]
+ public string Political { get; set; }
+
+ // payment_pricepoints
+
+ /// <summary>
+ /// The user's favorite athletes; this field is deprecated and will be removed in the near future
+ /// </summary>
+ [Obsolete]
+ [DataMember(Name = "favorite_athletes")]
+ public FacebookIdName[] FavoriteAthletes { get; set; }
+
+ /// <summary>
+ /// The user's favorite teams; this field is deprecated and will be removed in the near future
+ /// </summary>
+ [Obsolete]
+ [DataMember(Name = "favorite_teams")]
+ public FacebookIdName[] FavoriteTeams { get; set; }
+
+ /// <summary>
+ /// The URL of the user's profile pic (only returned if you explicitly specify a 'fields=picture' param)
+ /// If the "October 2012 Breaking Changes" migration setting is enabled for your app, this field will be an object with the url and is_silhouette fields; is_silhouette is true if the user has not uploaded a profile picture
+ /// </summary>
+ [DataMember(Name = "picture")]
+ public FacebookPicture Picture { get; set; }
+
+ /// <summary>
+ /// The user's favorite quotes
+ /// </summary>
+ [DataMember(Name = "quotes")]
+ public Uri Quotes { get; set; }
+
+ /// <summary>
+ /// The user's relationship status: Single, In a relationship, Engaged, Married, It's complicated, In an open relationship, Widowed, Separated, Divorced, In a civil union, In a domestic partnership
+ /// </summary>
+ [DataMember(Name = "relationship_status")]
+ public string RelationshipStatus { get; set; }
+
+ /// <summary>
+ /// The user's religion
+ /// </summary>
+ [DataMember(Name = "religion")]
+ public string Religion { get; set; }
+
+ // security_settings
+
+ /// <summary>
+ /// The user's significant other
+ /// </summary>
+ [DataMember(Name = "significant_other")]
+ public FacebookIdName SignificantOther { get; set; }
+
+ // video_upload_limits
+
+ /// <summary>
+ /// The URL of the user's personal website
+ /// </summary>
+ [DataMember(Name = "website")]
+ public Uri Website { get; set; }
+
+ // work
+
+ public DateTime? BirthdayDT {
+ get {
+ if (!string.IsNullOrEmpty(this.Birthday) && (this.Locale != null)) {
+ CultureInfo ci = new CultureInfo(this.Locale.Replace('_', '-'));
+ return DateTime.Parse(this.Birthday, ci);
+ }
+
+ return null;
+ }
+ }
+
+ public Uri AvatarUrl {
+ get {
+ if ((this.Picture != null) && (this.Picture.Data != null) && (this.Picture.Data.Url != null)) {
+ return this.Picture.Data.Url;
+ }
+
+ return null;
+ }
+ }
+
+ public HumanGender GenderEnum {
+ get {
+ if (this.Gender == "male") {
+ return HumanGender.Male;
+ } else if (this.Gender == "female") {
+ return HumanGender.Female;
+ }
+
+ return HumanGender.Unknown;
+ }
+ }
+
+ public static FacebookGraph Deserialize(string json) {
+ if (string.IsNullOrEmpty(json)) {
+ throw new ArgumentNullException("json");
+ }
+
+ return Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(json)));
+ }
+
+ public static FacebookGraph Deserialize(Stream jsonStream) {
+ if (jsonStream == null) {
+ throw new ArgumentNullException("jsonStream");
+ }
+
+ return (FacebookGraph)jsonSerializer.ReadObject(jsonStream);
+ }
+
+ public static class Fields {
+ public const string Defaults = "id,name,first_name,middle_name,last_name,gender,locale,link,username";
+
+ public const string Birthday = "locale,birthday";
+
+ public const string Email = "email";
+
+ public const string Picture = "picture";
+ }
+
+ /// <summary>
+ /// Obsolete: used only before October 2012
+ /// </summary>
+ [Obsolete]
+ [DataContract]
+ public class FacebookPicture {
+ [DataMember(Name = "data")]
+ public FacebookPictureData Data { get; set; }
+ }
+
+ /// <summary>
+ /// Obsolete: used only before October 2012
+ /// </summary>
+ [Obsolete]
+ [DataContract]
+ public class FacebookPictureData {
+ [DataMember(Name = "url")]
+ public Uri Url { get; set; }
+
+ [DataMember(Name = "is_silhouette")]
+ public bool IsSilhouette { get; set; }
+ }
+
+ [DataContract]
+ public class FacebookIdName {
+ [DataMember(Name = "id")]
+ public string Id { get; set; }
+
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+ }
+ }
+}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Google/GoogleClient.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Google/GoogleClient.cs
new file mode 100644
index 0000000..c623d2f
--- /dev/null
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Google/GoogleClient.cs
@@ -0,0 +1,107 @@
+//-----------------------------------------------------------------------
+// <copyright file="GoogleClient.cs" company="Outercurve Foundation">
+// Copyright (c) Outercurve Foundation. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.ApplicationBlock {
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OAuth2;
+
+ //// https://accounts.google.com/o/oauth2/auth
+
+ public class GoogleClient : WebServerClient {
+ private static readonly AuthorizationServerDescription GoogleDescription = new AuthorizationServerDescription {
+ TokenEndpoint = new Uri("https://accounts.google.com/o/oauth2/token"),
+ AuthorizationEndpoint = new Uri("https://accounts.google.com/o/oauth2/auth"),
+ //// RevokeEndpoint = new Uri("https://accounts.google.com/o/oauth2/revoke"),
+ ProtocolVersion = ProtocolVersion.V20
+ };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GoogleClient"/> class.
+ /// </summary>
+ public GoogleClient()
+ : base(GoogleDescription) {
+ }
+
+ public IOAuth2Graph GetGraph(IAuthorizationState authState, string[] fields = null) {
+ if ((authState != null) && (authState.AccessToken != null)) {
+ WebRequest request = WebRequest.Create("https://www.googleapis.com/oauth2/v1/userinfo?access_token=" + Uri.EscapeDataString(authState.AccessToken));
+ WebResponse response = request.GetResponse();
+
+ if (response != null) {
+ Stream responseStream = response.GetResponseStream();
+
+ if (responseStream != null) {
+ return GoogleGraph.Deserialize(responseStream);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Well-known scopes defined by Google.
+ /// </summary>
+ /// <remarks>
+ /// This sample includes just a few scopes. For a complete list of permissions please refer to:
+ /// https://developers.google.com/accounts/docs/OAuth2Login
+ /// </remarks>
+ public static class Scopes {
+ public static class UserInfo {
+ /// <summary>
+ /// Gain read-only access to basic profile information, including a user identifier, name, profile photo, profile URL, country, language, timezone, and birthdate.
+ /// </summary>
+ public const string Profile = "https://www.googleapis.com/auth/userinfo.profile";
+
+ /// <summary>
+ /// Gain read-only access to the user's email address.
+ /// </summary>
+ public const string Email = "https://www.googleapis.com/auth/userinfo.email";
+ }
+
+ public const string PlusMe = "https://www.googleapis.com/auth/plus.me";
+
+ public static class Drive {
+ /// <summary>
+ /// Full, permissive scope to access all of a user's files. Request this scope only when it is strictly necessary.
+ /// </summary>
+ public const string Default = "https://www.googleapis.com/auth/drive";
+
+ /// <summary>
+ /// Per-file access to files created or opened by the app
+ /// </summary>
+ public const string File = "https://www.googleapis.com/auth/drive.file";
+
+ /// <summary>
+ /// Allows apps read-only access to the list of Drive apps a user has installed
+ /// </summary>
+ public const string AppsReadonly = "https://www.googleapis.com/auth/drive.apps.readonly";
+
+ /// <summary>
+ /// Allows read-only access to file metadata and file content
+ /// </summary>
+ public const string Readonly = "https://www.googleapis.com/auth/drive.readonly";
+
+ /// <summary>
+ /// Allows read-only access to file metadata, but does not allow any access to read or download file content
+ /// </summary>
+ public const string Metadata = "https://www.googleapis.com/auth/drive.readonly.metadata";
+
+ /// <summary>
+ /// Special scope used to let users approve installation of an app
+ /// </summary>
+ public const string Install = "https://www.googleapis.com/auth/drive.install";
+ }
+ }
+ }
+}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Google/GoogleGraph.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Google/GoogleGraph.cs
new file mode 100644
index 0000000..69cfb22
--- /dev/null
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/Google/GoogleGraph.cs
@@ -0,0 +1,152 @@
+//-----------------------------------------------------------------------
+// <copyright file="GoogleGraph.cs" company="Andras Fuchs">
+// Copyright (c) Outercurve Foundation. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.ApplicationBlock {
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Runtime.Serialization;
+ using System.Runtime.Serialization.Json;
+ using System.Text;
+
+ //// Documentation: https://developers.google.com/accounts/docs/OAuth2Login
+
+ [DataContract]
+ public class GoogleGraph : IOAuth2Graph {
+ private static DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(GoogleGraph));
+
+ /// <summary>
+ /// Gets the value of this field is an immutable identifier for the logged-in user, and may be used when creating and managing user sessions in your application. This identifier is the same regardless of the client_id. This provides the ability to correlate profile information across multiple applications in the same organization. The value of this field is the same as the value of the userid field returned by the TokenInfo endpoint.
+ /// </summary>
+ [DataMember(Name = "id", IsRequired = true)]
+ public string Id { get; set; }
+
+ /// <summary>
+ /// Gets the email address of the logged in user
+ /// </summary>
+ [DataMember(Name = "email")]
+ public string Email { get; set; }
+
+ /// <summary>
+ /// Gets a flag that indicates whether or not Google has been able to verify the email address.
+ /// </summary>
+ [DataMember(Name = "verified_email")]
+ public bool? VerifiedEmail { get; set; }
+
+ /// <summary>
+ /// Gets the full name of the logged in user
+ /// </summary>
+ [DataMember(Name = "name", IsRequired = true)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// Gets the first name of the logged in user
+ /// </summary>
+ [DataMember(Name = "given_name")]
+ public string GivenName { get; set; }
+
+ /// <summary>
+ /// Gets the last name of the logged in user
+ /// </summary>
+ [DataMember(Name = "family_name")]
+ public string FamilyName { get; set; }
+
+ /// <summary>
+ /// Gets the URL to the user's profile picture. If the user has no public profile, this field is not included.
+ /// </summary>
+ [DataMember(Name = "picture")]
+ public Uri Picture { get; set; }
+
+ /// <summary>
+ /// Gets the user's registered locale. If the user has no public profile, this field is not included.
+ /// </summary>
+ [DataMember(Name = "locale")]
+ public string Locale { get; set; }
+
+ /// <summary>
+ /// Gets the default timezone of the logged in user
+ /// </summary>
+ [DataMember(Name = "timezone")]
+ public string Timezone { get; set; }
+
+ /// <summary>
+ /// Gets the gender of the logged in user (other|female|male)
+ /// </summary>
+ [DataMember(Name = "gender")]
+ public string Gender { get; set; }
+
+ [DataMember(Name = "birthday")]
+ public string Birthday { get; set; }
+
+ [DataMember(Name = "link")]
+ public Uri Link { get; set; }
+
+ public Uri AvatarUrl {
+ get {
+ return this.Picture;
+ }
+ }
+
+ public DateTime? BirthdayDT {
+ get {
+ if (!string.IsNullOrEmpty(this.Birthday) && (!this.Birthday.StartsWith("0000"))) {
+ return DateTime.ParseExact(this.Birthday, "yyyy-MM-dd", null);
+ }
+
+ return null;
+ }
+ }
+
+ public HumanGender GenderEnum {
+ get {
+ if (this.Gender == "male") {
+ return HumanGender.Male;
+ } else if (this.Gender == "female") {
+ return HumanGender.Female;
+ } else if (this.Gender == "other") {
+ return HumanGender.Other;
+ }
+
+ return HumanGender.Unknown;
+ }
+ }
+
+ public string FirstName {
+ get {
+ return this.GivenName;
+ }
+ }
+
+ public string LastName {
+ get {
+ return this.FamilyName;
+ }
+ }
+
+ public string UpdatedTime {
+ get {
+ return null;
+ }
+ }
+
+ public static GoogleGraph Deserialize(string json) {
+ if (string.IsNullOrEmpty(json)) {
+ throw new ArgumentNullException("json");
+ }
+
+ return Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(json)));
+ }
+
+ public static GoogleGraph Deserialize(Stream jsonStream) {
+ if (jsonStream == null) {
+ throw new ArgumentNullException("jsonStream");
+ }
+
+ return (GoogleGraph)jsonSerializer.ReadObject(jsonStream);
+ }
+ }
+}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/IOAuth2Graph.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/IOAuth2Graph.cs
new file mode 100644
index 0000000..925c616
--- /dev/null
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/IOAuth2Graph.cs
@@ -0,0 +1,34 @@
+namespace DotNetOpenAuth.ApplicationBlock {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+
+ public enum HumanGender { Unknown, Male, Female, Other }
+
+ public interface IOAuth2Graph {
+ string Id { get; }
+
+ Uri Link { get; }
+
+ string Name { get; }
+
+ string FirstName { get; }
+
+ string LastName { get; }
+
+ string Gender { get; }
+
+ string Locale { get; }
+
+ DateTime? BirthdayDT { get; }
+
+ string Email { get; }
+
+ Uri AvatarUrl { get; }
+
+ string UpdatedTime { get; }
+
+ HumanGender GenderEnum { get; }
+ }
+}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/WindowsLive/WindowsLiveClient.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/WindowsLive/WindowsLiveClient.cs
new file mode 100644
index 0000000..24322af
--- /dev/null
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/WindowsLive/WindowsLiveClient.cs
@@ -0,0 +1,181 @@
+//-----------------------------------------------------------------------
+// <copyright file="WindowsLiveClient.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.ApplicationBlock {
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Net;
+ using System.Text;
+ using System.Web;
+ using DotNetOpenAuth.Messaging;
+ using DotNetOpenAuth.OAuth2;
+
+ public class WindowsLiveClient : WebServerClient {
+ private static readonly AuthorizationServerDescription WindowsLiveDescription = new AuthorizationServerDescription {
+ TokenEndpoint = new Uri("https://oauth.live.com/token"),
+ AuthorizationEndpoint = new Uri("https://oauth.live.com/authorize"),
+ ProtocolVersion = ProtocolVersion.V20
+ };
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WindowsLiveClient"/> class.
+ /// </summary>
+ public WindowsLiveClient()
+ : base(WindowsLiveDescription) {
+ }
+
+ public IOAuth2Graph GetGraph(IAuthorizationState authState, string[] fields = null) {
+ if ((authState != null) && (authState.AccessToken != null)) {
+ WebRequest request = WebRequest.Create("https://apis.live.net/v5.0/me?access_token=" + Uri.EscapeDataString(authState.AccessToken));
+ WebResponse response = request.GetResponse();
+
+ if (response != null) {
+ Stream responseStream = response.GetResponseStream();
+
+ if (responseStream != null) {
+ // string debugJsonStr = new StreamReader(responseStream).ReadToEnd();
+ WindowsLiveGraph windowsLiveGraph = WindowsLiveGraph.Deserialize(responseStream);
+
+ // picture type resolution test 1
+ // &type=small 96x96
+ // &type=medium 96x96
+ // &type=large 448x448
+
+ windowsLiveGraph.AvatarUrl = new Uri("https://apis.live.net/v5.0/me/picture?access_token=" + Uri.EscapeDataString(authState.AccessToken));
+
+ return windowsLiveGraph;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /// <summary>
+ /// Well-known scopes defined by the Windows Live service.
+ /// </summary>
+ /// <remarks>
+ /// This sample includes just a few scopes. For a complete list of scopes please refer to:
+ /// http://msdn.microsoft.com/en-us/library/hh243646.aspx
+ /// </remarks>
+ public static class Scopes {
+ #region Core Scopes
+
+ /// <summary>
+ /// The ability of an app to read and update a user's info at any time. Without this scope, an app can access the user's info only while the user is signed in to Live Connect and is using your app.
+ /// </summary>
+ public const string OfflineAccess = "wl.offline_access";
+
+ /// <summary>
+ /// Single sign-in behavior. With single sign-in, users who are already signed in to Live Connect are also signed in to your website.
+ /// </summary>
+ public const string SignIn = "wl.signin";
+
+ /// <summary>
+ /// Read access to a user's basic profile info. Also enables read access to a user's list of contacts.
+ /// </summary>
+ public const string Basic = "wl.basic";
+
+ #endregion
+
+ #region Extended Scopes
+
+ /// <summary>
+ /// Read access to a user's birthday info including birth day, month, and year.
+ /// </summary>
+ public const string Birthday = "wl.birthday";
+
+ /// <summary>
+ /// Read access to a user's calendars and events.
+ /// </summary>
+ public const string Calendars = "wl.calendars";
+
+ /// <summary>
+ /// Read and write access to a user's calendars and events.
+ /// </summary>
+ public const string CalendarsUpdate = "wl.calendars_update";
+
+ /// <summary>
+ /// Read access to the birth day and birth month of a user's contacts. Note that this also gives read access to the user's birth day, birth month, and birth year.
+ /// </summary>
+ public const string ContactsBirthday = "wl.contacts_birthday";
+
+ /// <summary>
+ /// Creation of new contacts in the user's address book.
+ /// </summary>
+ public const string ContactsCreate = "wl.contacts_create";
+
+ /// <summary>
+ /// Read access to a user's calendars and events. Also enables read access to any calendars and events that other users have shared with the user.
+ /// </summary>
+ public const string ContactsCalendars = "wl.contacts_calendars";
+
+ /// <summary>
+ /// Read access to a user's albums, photos, videos, and audio, and their associated comments and tags. Also enables read access to any albums, photos, videos, and audio that other users have shared with the user.
+ /// </summary>
+ public const string ContactsPhotos = "wl.contacts_photos";
+
+ /// <summary>
+ /// Read access to Microsoft SkyDrive files that other users have shared with the user. Note that this also gives read access to the user's files stored in SkyDrive.
+ /// </summary>
+ public const string ContactsSkydrive = "wl.contacts_skydrive";
+
+ /// <summary>
+ /// Read access to a user's personal, preferred, and business email addresses.
+ /// </summary>
+ public const string Emails = "wl.emails";
+
+ /// <summary>
+ /// Creation of events on the user's default calendar.
+ /// </summary>
+ public const string EventsCreate = "wl.events_create";
+
+ /// <summary>
+ /// Enables signing in to the Windows Live Messenger Extensible Messaging and Presence Protocol (XMPP) service.
+ /// </summary>
+ public const string Messenger = "wl.messenger";
+
+ /// <summary>
+ /// Read access to a user's personal, business, and mobile phone numbers.
+ /// </summary>
+ public const string PhoneNumbers = "wl.phone_numbers";
+
+ /// <summary>
+ /// Read access to a user's photos, videos, audio, and albums.
+ /// </summary>
+ public const string Photos = "wl.photos";
+
+ /// <summary>
+ /// Read access to a user's postal addresses.
+ /// </summary>
+ public const string PostalAddresses = "wl.postal_addresses";
+
+ /// <summary>
+ /// Enables updating a user's status message.
+ /// </summary>
+ public const string Share = "wl.share";
+
+ /// <summary>
+ /// Read access to a user's files stored in SkyDrive.
+ /// </summary>
+ public const string Skydrive = "wl.skydrive";
+
+ /// <summary>
+ /// Read and write access to a user's files stored in SkyDrive.
+ /// </summary>
+ public const string SkydriveUpdate = "wl.skydrive_update";
+
+ /// <summary>
+ /// Read access to a user's employer and work position information.
+ /// </summary>
+ public const string WorkProfile = "wl.work_profile";
+
+ #endregion
+ }
+ }
+}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/WindowsLive/WindowsLiveGraph.cs b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/WindowsLive/WindowsLiveGraph.cs
new file mode 100644
index 0000000..38095a5
--- /dev/null
+++ b/samples/DotNetOpenAuth.ApplicationBlock/OAuth2/WindowsLive/WindowsLiveGraph.cs
@@ -0,0 +1,299 @@
+//-----------------------------------------------------------------------
+// <copyright file="WindowsLiveGraph.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.ApplicationBlock {
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Linq;
+ using System.Runtime.Serialization;
+ using System.Runtime.Serialization.Json;
+ using System.Text;
+
+ //// Documentation: http://msdn.microsoft.com/en-us/library/live/hh243648.aspx#user
+
+ [DataContract]
+ public class WindowsLiveGraph : IOAuth2Graph {
+ private static DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(WindowsLiveGraph));
+
+ /// <summary>
+ /// The user's ID.
+ /// </summary>
+ [DataMember(Name = "id", IsRequired = true)]
+ public string Id { get; set; }
+
+ /// <summary>
+ /// The user's full name.
+ /// </summary>
+ [DataMember(Name = "name", IsRequired = true)]
+ public string Name { get; set; }
+
+ /// <summary>
+ /// The user's first name.
+ /// </summary>
+ [DataMember(Name = "first_name")]
+ public string FirstName { get; set; }
+
+ /// <summary>
+ /// The user's last name.
+ /// </summary>
+ [DataMember(Name = "last_name")]
+ public string LastName { get; set; }
+
+ /// <summary>
+ /// The URL of the user's profile page.
+ /// </summary>
+ [DataMember(Name = "link")]
+ public Uri Link { get; set; }
+
+ /// <summary>
+ /// The day of the user's birth date, or null if no birth date is specified.
+ /// </summary>
+ [DataMember(Name = "birth_day")]
+ public int? BirthDay { get; set; }
+
+ /// <summary>
+ /// The month of the user's birth date, or null if no birth date is specified.
+ /// </summary>
+ [DataMember(Name = "birth_month")]
+ public int? BirthMonth { get; set; }
+
+ /// <summary>
+ /// The year of the user's birth date, or null if no birth date is specified.
+ /// </summary>
+ [DataMember(Name = "birth_year")]
+ public int? BirthYear { get; set; }
+
+ /// <summary>
+ /// An array that contains the user's work info.
+ /// </summary>
+ [DataMember(Name = "work")]
+ public WindowsLiveWorkProfile[] Work { get; set; }
+
+ /// <summary>
+ /// The user's gender. Valid values are "male", "female", or null if the user's gender is not specified.
+ /// </summary>
+ [DataMember(Name = "gender")]
+ public string Gender { get; set; }
+
+ /// <summary>
+ /// The user's email addresses.
+ /// </summary>
+ [DataMember(Name = "emails")]
+ public WindowsLiveEmails Emails { get; set; }
+
+ /// <summary>
+ /// The user's postal addresses.
+ /// </summary>
+ [DataMember(Name = "addresses")]
+ public WindowsLiveAddresses Addresses { get; set; }
+
+ /// <summary>
+ /// The user's phone numbers.
+ /// </summary>
+ [DataMember(Name = "phones")]
+ public WindowsLivePhones Phones { get; set; }
+
+ /// <summary>
+ /// The user's locale code.
+ /// </summary>
+ [DataMember(Name = "locale", IsRequired = true)]
+ public string Locale { get; set; }
+
+ /// <summary>
+ /// The time, in ISO 8601 format, at which the user last updated the object.
+ /// </summary>
+ [DataMember(Name = "updated_time")]
+ public string UpdatedTime { get; set; }
+
+ public string Email {
+ get {
+ return this.Emails.Account;
+ }
+ }
+
+ public Uri AvatarUrl { get; set; }
+
+ public DateTime? BirthdayDT {
+ get {
+ if (this.BirthYear.HasValue && this.BirthMonth.HasValue && this.BirthDay.HasValue) {
+ return new DateTime(this.BirthYear.Value, this.BirthMonth.Value, this.BirthDay.Value);
+ }
+
+ return null;
+ }
+ }
+
+ public HumanGender GenderEnum {
+ get {
+ if (this.Gender == "male") {
+ return HumanGender.Male;
+ } else if (this.Gender == "female") {
+ return HumanGender.Female;
+ }
+
+ return HumanGender.Unknown;
+ }
+ }
+
+ public static WindowsLiveGraph Deserialize(string json) {
+ if (string.IsNullOrEmpty(json)) {
+ throw new ArgumentNullException("json");
+ }
+
+ return Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(json)));
+ }
+
+ public static WindowsLiveGraph Deserialize(Stream jsonStream) {
+ if (jsonStream == null) {
+ throw new ArgumentNullException("jsonStream");
+ }
+
+ return (WindowsLiveGraph)jsonSerializer.ReadObject(jsonStream);
+ }
+
+ [DataContract]
+ public class WindowsLiveEmails {
+ /// <summary>
+ /// The user's preferred email address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "preferred")]
+ public string Preferred { get; set; }
+
+ /// <summary>
+ /// The email address that is associated with the account.
+ /// </summary>
+ [DataMember(Name = "account", IsRequired = true)]
+ public string Account { get; set; }
+
+ /// <summary>
+ /// The user's personal email address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "personal")]
+ public string Personal { get; set; }
+
+ /// <summary>
+ /// The user's business email address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "business")]
+ public string Business { get; set; }
+
+ /// <summary>
+ /// The user's "alternate" email address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "other")]
+ public string Other { get; set; }
+ }
+
+ [DataContract]
+ public class WindowsLivePhones {
+ /// <summary>
+ /// The user's personal phone number, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "personal")]
+ public string Personal { get; set; }
+
+ /// <summary>
+ /// The user's business phone number, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "business")]
+ public string Business { get; set; }
+
+ /// <summary>
+ /// The user's mobile phone number, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "mobile")]
+ public string Mobile { get; set; }
+ }
+
+ [DataContract]
+ public class WindowsLiveAddress {
+ /// <summary>
+ /// The street address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "street")]
+ public string Street { get; set; }
+
+ /// <summary>
+ /// The second line of the street address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "street_2")]
+ public string Street2 { get; set; }
+
+ /// <summary>
+ /// The city of the address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "city")]
+ public string City { get; set; }
+
+ /// <summary>
+ /// The state of the address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "state")]
+ public string State { get; set; }
+
+ /// <summary>
+ /// The postal code of the address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "postal_code")]
+ public string PostalCode { get; set; }
+
+ /// <summary>
+ /// The region of the address, or null if one is not specified.
+ /// </summary>
+ [DataMember(Name = "region")]
+ public string Region { get; set; }
+ }
+
+ [DataContract]
+ public class WindowsLiveAddresses {
+ /// <summary>
+ /// The user's personal postal address.
+ /// </summary>
+ [DataMember(Name = "personal")]
+ public WindowsLiveAddress Personal { get; set; }
+
+ /// <summary>
+ /// The user's business postal address.
+ /// </summary>
+ [DataMember(Name = "business")]
+ public WindowsLiveAddress Business { get; set; }
+ }
+
+ [DataContract]
+ public class WindowsLiveWorkProfile {
+ /// <summary>
+ /// Info about the user's employer.
+ /// </summary>
+ [DataMember(Name = "employer")]
+ public WindowsLiveEmployer Employer { get; set; }
+
+ /// <summary>
+ /// Info about the user's employer.
+ /// </summary>
+ [DataMember(Name = "position")]
+ public WindowsLivePosition Position { get; set; }
+ }
+
+ [DataContract]
+ public class WindowsLiveEmployer {
+ /// <summary>
+ /// The name of the user's employer, or null if the employer's name is not specified.
+ /// </summary>
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+ }
+
+ [DataContract]
+ public class WindowsLivePosition {
+ /// <summary>
+ /// The name of the user's work position, or null if the name of the work position is not specified.
+ /// </summary>
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+ }
+ }
+}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/WindowsLiveClient.cs b/samples/DotNetOpenAuth.ApplicationBlock/WindowsLiveClient.cs
deleted file mode 100644
index a2e1058..0000000
--- a/samples/DotNetOpenAuth.ApplicationBlock/WindowsLiveClient.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="WindowsLiveClient.cs" company="Andrew Arnott">
-// Copyright (c) Andrew Arnott. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOpenAuth.ApplicationBlock {
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using DotNetOpenAuth.OAuth2;
-
- public class WindowsLiveClient : WebServerClient {
- private static readonly AuthorizationServerDescription WindowsLiveDescription = new AuthorizationServerDescription {
- TokenEndpoint = new Uri("https://oauth.live.com/token"),
- AuthorizationEndpoint = new Uri("https://oauth.live.com/authorize"),
- };
-
- /// <summary>
- /// Initializes a new instance of the <see cref="WindowsLiveClient"/> class.
- /// </summary>
- public WindowsLiveClient()
- : base(WindowsLiveDescription) {
- }
-
- /// <summary>
- /// Well-known scopes defined by the Windows Live service.
- /// </summary>
- /// <remarks>
- /// This sample includes just a few scopes. For a complete list of scopes please refer to:
- /// http://msdn.microsoft.com/en-us/library/hh243646.aspx
- /// </remarks>
- public static class Scopes {
- /// <summary>
- /// The ability of an app to read and update a user's info at any time. Without this scope, an app can access the user's info only while the user is signed in to Live Connect and is using your app.
- /// </summary>
- public const string OfflineAccess = "wl.offline_access";
-
- /// <summary>
- /// Single sign-in behavior. With single sign-in, users who are already signed in to Live Connect are also signed in to your website.
- /// </summary>
- public const string SignIn = "wl.signin";
-
- /// <summary>
- /// Read access to a user's basic profile info. Also enables read access to a user's list of contacts.
- /// </summary>
- public const string Basic = "wl.basic";
- }
- }
-}
diff --git a/samples/DotNetOpenAuth.ApplicationBlock/WindowsLiveGraph.cs b/samples/DotNetOpenAuth.ApplicationBlock/WindowsLiveGraph.cs
deleted file mode 100644
index 4801226..0000000
--- a/samples/DotNetOpenAuth.ApplicationBlock/WindowsLiveGraph.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-//-----------------------------------------------------------------------
-// <copyright file="WindowsLiveGraph.cs" company="Andrew Arnott">
-// Copyright (c) Andrew Arnott. All rights reserved.
-// </copyright>
-//-----------------------------------------------------------------------
-
-namespace DotNetOpenAuth.ApplicationBlock {
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.Linq;
- using System.Runtime.Serialization;
- using System.Runtime.Serialization.Json;
- using System.Text;
-
- [DataContract]
- public class WindowsLiveGraph {
- private static DataContractJsonSerializer jsonSerializer = new DataContractJsonSerializer(typeof(WindowsLiveGraph));
-
- [DataMember(Name = "id")]
- public string Id { get; set; }
-
- [DataMember(Name = "name")]
- public string Name { get; set; }
-
- [DataMember(Name = "first_name")]
- public string FirstName { get; set; }
-
- [DataMember(Name = "last_name")]
- public string LastName { get; set; }
-
- [DataMember(Name = "link")]
- public Uri Link { get; set; }
-
- [DataMember(Name = "gender")]
- public string Gender { get; set; }
-
- [DataMember(Name = "updated_time")]
- public string UpdatedTime { get; set; }
-
- [DataMember(Name = "locale")]
- public string Locale { get; set; }
-
- public static WindowsLiveGraph Deserialize(string json) {
- if (string.IsNullOrEmpty(json)) {
- throw new ArgumentNullException("json");
- }
-
- return Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(json)));
- }
-
- public static WindowsLiveGraph Deserialize(Stream jsonStream) {
- if (jsonStream == null) {
- throw new ArgumentNullException("jsonStream");
- }
-
- return (WindowsLiveGraph)jsonSerializer.ReadObject(jsonStream);
- }
- }
-}
diff --git a/samples/OAuthClient/Default.aspx b/samples/OAuthClient/Default.aspx
index 79d0acf..d04066d 100644
--- a/samples/OAuthClient/Default.aspx
+++ b/samples/OAuthClient/Default.aspx
@@ -9,9 +9,10 @@
<li><a href="GoogleAddressBook.aspx">Download your Gmail address book</a></li>
<li><a href="Twitter.aspx">Get your Twitter updates</a></li>
<li><a href="SignInWithTwitter.aspx">Sign In With Twitter</a></li>
- <li><a href="Facebook.aspx">Sign in with Facebook</a></li>
- <li><a href="WindowsLive.aspx">Sign in with Windows Live</a></li>
<li><a href="AzureAD.aspx">Sign in with Azure Active Directory\Office 365</a></li>
+ <li><a href="Facebook.aspx">Sign in with Facebook (OAuth 2.0)</a></li>
+ <li><a href="WindowsLive.aspx">Sign in with Windows Live (OAuth 2.0)</a></li>
+ <li><a href="Google.aspx">Sign in with Google (OAuth 2.0) [check your web.config and set the googleClientID and googleClientSecret values before testing]</a></li>
<li><a href="SampleWcf2.aspx">Interop with Authorization Server sample (Authorization code grant) and Resource Server using WCF w/ OAuth 2.0 </a></li>
<li><a href="SampleWcf2Javascript.html">Interop with Authorization Server sample (implicit grant) and Resource Server using WCF w/ OAuth 2.0 </a></li>
</ul>
diff --git a/samples/OAuthClient/Facebook.aspx.cs b/samples/OAuthClient/Facebook.aspx.cs
index 4701d24..e7261a0 100644
--- a/samples/OAuthClient/Facebook.aspx.cs
+++ b/samples/OAuthClient/Facebook.aspx.cs
@@ -1,31 +1,37 @@
-namespace OAuthClient {
+namespace OAuthClient
+{
using System;
using System.Configuration;
using System.Net;
using System.Web;
using DotNetOpenAuth.ApplicationBlock;
- using DotNetOpenAuth.ApplicationBlock.Facebook;
using DotNetOpenAuth.OAuth2;
- public partial class Facebook : System.Web.UI.Page {
- private static readonly FacebookClient client = new FacebookClient {
+ public partial class Facebook : System.Web.UI.Page
+ {
+ private static readonly FacebookClient facebookClient = new FacebookClient
+ {
ClientIdentifier = ConfigurationManager.AppSettings["facebookAppID"],
ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(ConfigurationManager.AppSettings["facebookAppSecret"]),
};
- protected void Page_Load(object sender, EventArgs e) {
- IAuthorizationState authorization = client.ProcessUserAuthorization();
- if (authorization == null) {
+ protected void Page_Load(object sender, EventArgs e)
+ {
+ IAuthorizationState authorization = facebookClient.ProcessUserAuthorization();
+ if (authorization == null)
+ {
// Kick off authorization request
- client.RequestUserAuthorization();
- } else {
- var request = WebRequest.Create("https://graph.facebook.com/me?access_token=" + Uri.EscapeDataString(authorization.AccessToken));
- using (var response = request.GetResponse()) {
- using (var responseStream = response.GetResponseStream()) {
- var graph = FacebookGraph.Deserialize(responseStream);
- this.nameLabel.Text = HttpUtility.HtmlEncode(graph.Name);
- }
- }
+ facebookClient.RequestUserAuthorization();
+
+ // alternatively you can ask for more information
+ // facebookClient.RequestUserAuthorization(scope: new[] { FacebookClient.Scopes.Email, FacebookClient.Scopes.UserBirthday });
+ }
+ else
+ {
+ IOAuth2Graph oauth2Graph = facebookClient.GetGraph(authorization);
+ // IOAuth2Graph oauth2Graph = facebookClient.GetGraph(authorization, new[] { FacebookGraph.Fields.Defaults, FacebookGraph.Fields.Email, FacebookGraph.Fields.Picture, FacebookGraph.Fields.Birthday });
+
+ this.nameLabel.Text = HttpUtility.HtmlEncode(oauth2Graph.Name);
}
}
}
diff --git a/samples/OAuthClient/Google.aspx b/samples/OAuthClient/Google.aspx
new file mode 100644
index 0000000..a9eae90
--- /dev/null
+++ b/samples/OAuthClient/Google.aspx
@@ -0,0 +1,16 @@
+<%@ Page Language="C#" AutoEventWireup="true" Inherits="OAuthClient.Google" Codebehind="Google.aspx.cs" %>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head runat="server">
+ <title></title>
+</head>
+<body>
+ <form id="form1" runat="server">
+ <div>
+ Welcome,
+ <asp:Label Text="[name]" ID="nameLabel" runat="server" />
+ </div>
+ </form>
+</body>
+</html>
diff --git a/samples/OAuthClient/Google.aspx.cs b/samples/OAuthClient/Google.aspx.cs
new file mode 100644
index 0000000..fda643e
--- /dev/null
+++ b/samples/OAuthClient/Google.aspx.cs
@@ -0,0 +1,37 @@
+namespace OAuthClient
+{
+ using System;
+ using System.Configuration;
+ using System.Net;
+ using System.Web;
+ using DotNetOpenAuth.ApplicationBlock;
+ using DotNetOpenAuth.OAuth2;
+
+ public partial class Google : System.Web.UI.Page
+ {
+ private static readonly GoogleClient googleClient = new GoogleClient
+ {
+ ClientIdentifier = ConfigurationManager.AppSettings["googleClientID"],
+ ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(ConfigurationManager.AppSettings["googleClientSecret"]),
+ };
+
+ protected void Page_Load(object sender, EventArgs e)
+ {
+ IAuthorizationState authorization = googleClient.ProcessUserAuthorization();
+ if (authorization == null)
+ {
+ // Kick off authorization request
+ googleClient.RequestUserAuthorization();
+
+ // alternatively you can ask for more information
+ // googleClient.RequestUserAuthorization(scope: new[] { GoogleClient.Scopes.UserInfo.Profile, GoogleClient.Scopes.UserInfo.Email });
+ }
+ else
+ {
+ IOAuth2Graph oauth2Graph = googleClient.GetGraph(authorization);
+
+ this.nameLabel.Text = HttpUtility.HtmlEncode(oauth2Graph.Name);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/samples/OAuthClient/Google.aspx.designer.cs b/samples/OAuthClient/Google.aspx.designer.cs
new file mode 100644
index 0000000..4c7ecad
--- /dev/null
+++ b/samples/OAuthClient/Google.aspx.designer.cs
@@ -0,0 +1,33 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+namespace OAuthClient {
+
+
+ public partial class Google {
+
+ /// <summary>
+ /// form1 control.
+ /// </summary>
+ /// <remarks>
+ /// Auto-generated field.
+ /// To modify move field declaration from designer file to code-behind file.
+ /// </remarks>
+ protected global::System.Web.UI.HtmlControls.HtmlForm form1;
+
+ /// <summary>
+ /// nameLabel control.
+ /// </summary>
+ /// <remarks>
+ /// Auto-generated field.
+ /// To modify move field declaration from designer file to code-behind file.
+ /// </remarks>
+ protected global::System.Web.UI.WebControls.Label nameLabel;
+ }
+}
diff --git a/samples/OAuthClient/OAuthClient.csproj b/samples/OAuthClient/OAuthClient.csproj
index 9fbcc5d..3172562 100644
--- a/samples/OAuthClient/OAuthClient.csproj
+++ b/samples/OAuthClient/OAuthClient.csproj
@@ -10,6 +10,7 @@
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
+ <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\..\src\</SolutionDir>
</PropertyGroup>
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
@@ -23,7 +24,7 @@
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>OAuthClient</RootNamespace>
<AssemblyName>OAuthClient</AssemblyName>
- <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+ <TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<TargetFrameworkProfile />
<UseIISExpress>true</UseIISExpress>
</PropertyGroup>
@@ -49,7 +50,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="log4net">
- <HintPath>..\..\lib\log4net.dll</HintPath>
+ <HintPath>..\..\src\packages\log4net.2.0.0\lib\net40-full\log4net.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
@@ -71,6 +72,7 @@
</ItemGroup>
<ItemGroup>
<Content Include="AzureAD.aspx" />
+ <Content Include="Google.aspx" />
<Content Include="Default.aspx" />
<Content Include="Facebook.aspx" />
<Content Include="favicon.ico" />
@@ -84,6 +86,7 @@
<Content Include="Scripts\jquery-1.6.1.min.js" />
<Content Include="WindowsLive.aspx" />
<Content Include="Yammer.aspx" />
+ <Content Include="packages.config" />
<None Include="Service References\SampleResourceServer\DataApi.disco" />
<None Include="Service References\SampleResourceServer\configuration91.svcinfo" />
<None Include="Service References\SampleResourceServer\configuration.svcinfo" />
@@ -114,6 +117,13 @@
<Compile Include="AzureAD.aspx.designer.cs">
<DependentUpon>AzureAD.aspx</DependentUpon>
</Compile>
+ <Compile Include="Google.aspx.cs">
+ <DependentUpon>Google.aspx</DependentUpon>
+ <SubType>ASPXCodeBehind</SubType>
+ </Compile>
+ <Compile Include="Google.aspx.designer.cs">
+ <DependentUpon>Google.aspx</DependentUpon>
+ </Compile>
<Compile Include="Facebook.aspx.cs">
<DependentUpon>Facebook.aspx</DependentUpon>
<SubType>ASPXCodeBehind</SubType>
@@ -262,4 +272,5 @@
</VisualStudio>
</ProjectExtensions>
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))\EnlistmentInfo.targets" Condition=" '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), EnlistmentInfo.targets))' != '' " />
+ <Import Project="$(SolutionDir)\.nuget\nuget.targets" />
</Project> \ No newline at end of file
diff --git a/samples/OAuthClient/Web.config b/samples/OAuthClient/Web.config
index 9463ced..35984fc 100644
--- a/samples/OAuthClient/Web.config
+++ b/samples/OAuthClient/Web.config
@@ -46,6 +46,9 @@
<!-- Google sign-up: https://www.google.com/accounts/ManageDomains -->
<add key="googleConsumerKey" value="anonymous"/>
<add key="googleConsumerSecret" value="anonymous"/>
+ <!--Google API sign-up: https://code.google.com/apis/console/ (OAuth2) -->
+ <add key="googleClientID" value="" />
+ <add key="googleClientSecret" value="" />
<!-- Yammer sign-up: https://www.yammer.com/client_applications/new -->
<add key="yammerConsumerKey" value=""/>
<add key="yammerConsumerSecret" value=""/>
diff --git a/samples/OAuthClient/WindowsLive.aspx.cs b/samples/OAuthClient/WindowsLive.aspx.cs
index 05101a7..b28bcc3 100644
--- a/samples/OAuthClient/WindowsLive.aspx.cs
+++ b/samples/OAuthClient/WindowsLive.aspx.cs
@@ -1,4 +1,5 @@
-namespace OAuthClient {
+namespace OAuthClient
+{
using System;
using System.Collections.Generic;
using System.Configuration;
@@ -8,36 +9,42 @@
using System.Web.UI;
using System.Web.UI.WebControls;
using DotNetOpenAuth.ApplicationBlock;
- using DotNetOpenAuth.ApplicationBlock.Facebook;
using DotNetOpenAuth.OAuth2;
- public partial class WindowsLive : System.Web.UI.Page {
- private static readonly WindowsLiveClient client = new WindowsLiveClient {
+ public partial class WindowsLive : System.Web.UI.Page
+ {
+ private static readonly WindowsLiveClient windowsLiveClient = new WindowsLiveClient
+ {
ClientIdentifier = ConfigurationManager.AppSettings["windowsLiveAppID"],
ClientCredentialApplicator = ClientCredentialApplicator.PostParameter(ConfigurationManager.AppSettings["WindowsLiveAppSecret"]),
};
- protected void Page_Load(object sender, EventArgs e) {
- if (string.Equals("localhost", this.Request.Headers["Host"].Split(':')[0], StringComparison.OrdinalIgnoreCase)) {
+ protected void Page_Load(object sender, EventArgs e)
+ {
+ if (string.Equals("localhost", this.Request.Headers["Host"].Split(':')[0], StringComparison.OrdinalIgnoreCase))
+ {
this.localhostDoesNotWorkPanel.Visible = true;
var builder = new UriBuilder(this.publicLink.NavigateUrl);
builder.Port = this.Request.Url.Port;
this.publicLink.NavigateUrl = builder.Uri.AbsoluteUri;
this.publicLink.Text = builder.Uri.AbsoluteUri;
- } else {
- IAuthorizationState authorization = client.ProcessUserAuthorization();
- if (authorization == null) {
+ }
+ else
+ {
+ IAuthorizationState authorization = windowsLiveClient.ProcessUserAuthorization();
+ if (authorization == null)
+ {
// Kick off authorization request
- client.RequestUserAuthorization(scope: new[] { WindowsLiveClient.Scopes.Basic }); // this scope isn't even required just to log in
- } else {
- var request =
- WebRequest.Create("https://apis.live.net/v5.0/me?access_token=" + Uri.EscapeDataString(authorization.AccessToken));
- using (var response = request.GetResponse()) {
- using (var responseStream = response.GetResponseStream()) {
- var graph = WindowsLiveGraph.Deserialize(responseStream);
- this.nameLabel.Text = HttpUtility.HtmlEncode(graph.Name);
- }
- }
+ windowsLiveClient.RequestUserAuthorization(scope: new[] { WindowsLiveClient.Scopes.Basic }); // this scope isn't even required just to log in
+
+ // alternatively you can ask for more information
+ // windowsLiveClient.RequestUserAuthorization(scope: new[] { WindowsLiveClient.Scopes.SignIn, WindowsLiveClient.Scopes.Emails, WindowsLiveClient.Scopes.Birthday });
+ }
+ else
+ {
+ IOAuth2Graph oauth2Graph = windowsLiveClient.GetGraph(authorization);
+
+ this.nameLabel.Text = HttpUtility.HtmlEncode(oauth2Graph.Name);
}
}
}
diff --git a/samples/OAuthClient/packages.config b/samples/OAuthClient/packages.config
new file mode 100644
index 0000000..6562527
--- /dev/null
+++ b/samples/OAuthClient/packages.config
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+ <package id="log4net" version="2.0.0" targetFramework="net45" />
+</packages> \ No newline at end of file