diff options
Diffstat (limited to 'src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements')
18 files changed, 1627 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessRequestBindingElement.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessRequestBindingElement.cs new file mode 100644 index 0000000..b86f5dd --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessRequestBindingElement.cs @@ -0,0 +1,158 @@ +//----------------------------------------------------------------------- +// <copyright file="AccessRequestBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Decodes verification codes, refresh tokens and access tokens on incoming messages. + /// </summary> + /// <remarks> + /// This binding element also ensures that the code/token coming in is issued to + /// the same client that is sending the code/token and that the authorization has + /// not been revoked and that an access token has not expired. + /// </remarks> + internal class AccessRequestBindingElement : AuthServerBindingElementBase { + /// <summary> + /// Initializes a new instance of the <see cref="AccessRequestBindingElement"/> class. + /// </summary> + internal AccessRequestBindingElement() { + } + + /// <summary> + /// Gets the protection commonly offered (if any) by this binding element. + /// </summary> + /// <value></value> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + public override MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public override MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var response = message as IAuthorizationCarryingRequest; + if (response != null) { + switch (response.CodeOrTokenType) { + case CodeOrTokenType.AuthorizationCode: + var codeFormatter = AuthorizationCode.CreateFormatter(this.AuthorizationServer); + var code = (AuthorizationCode)response.AuthorizationDescription; + response.CodeOrToken = codeFormatter.Serialize(code); + break; + case CodeOrTokenType.AccessToken: + var responseWithOriginatingRequest = (IDirectResponseProtocolMessage)message; + var request = (IAccessTokenRequest)responseWithOriginatingRequest.OriginatingRequest; + + using (var resourceServerKey = this.AuthorizationServer.GetResourceServerEncryptionKey(request)) { + var tokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServer.AccessTokenSigningKey, resourceServerKey); + var token = (AccessToken)response.AuthorizationDescription; + response.CodeOrToken = tokenFormatter.Serialize(token); + break; + } + default: + throw ErrorUtilities.ThrowInternal(string.Format(CultureInfo.CurrentCulture, "Unexpected outgoing code or token type: {0}", response.CodeOrTokenType)); + } + + return MessageProtections.None; + } + + var accessTokenResponse = message as AccessTokenSuccessResponse; + if (accessTokenResponse != null) { + var directResponseMessage = (IDirectResponseProtocolMessage)accessTokenResponse; + var accessTokenRequest = (AccessTokenRequestBase)directResponseMessage.OriginatingRequest; + ErrorUtilities.VerifyProtocol(accessTokenRequest.GrantType != GrantType.ClientCredentials || accessTokenResponse.RefreshToken == null, OAuthStrings.NoGrantNoRefreshToken); + } + + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public override MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var tokenRequest = message as IAuthorizationCarryingRequest; + if (tokenRequest != null) { + try { + switch (tokenRequest.CodeOrTokenType) { + case CodeOrTokenType.AuthorizationCode: + var verificationCodeFormatter = AuthorizationCode.CreateFormatter(this.AuthorizationServer); + var verificationCode = verificationCodeFormatter.Deserialize(message, tokenRequest.CodeOrToken); + tokenRequest.AuthorizationDescription = verificationCode; + break; + case CodeOrTokenType.RefreshToken: + var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.CryptoKeyStore); + var refreshToken = refreshTokenFormatter.Deserialize(message, tokenRequest.CodeOrToken); + tokenRequest.AuthorizationDescription = refreshToken; + break; + default: + throw ErrorUtilities.ThrowInternal("Unexpected value for CodeOrTokenType: " + tokenRequest.CodeOrTokenType); + } + } catch (ExpiredMessageException ex) { + throw ErrorUtilities.Wrap(ex, Protocol.authorization_expired); + } + + var accessRequest = tokenRequest as AccessTokenRequestBase; + if (accessRequest != null) { + // Make sure the client sending us this token is the client we issued the token to. + ErrorUtilities.VerifyProtocol(string.Equals(accessRequest.ClientIdentifier, tokenRequest.AuthorizationDescription.ClientIdentifier, StringComparison.Ordinal), Protocol.incorrect_client_credentials); + + // Check that the client secret is correct. + var client = this.AuthorizationServer.GetClientOrThrow(accessRequest.ClientIdentifier); + string secret = client.Secret; + ErrorUtilities.VerifyProtocol(!String.IsNullOrEmpty(secret), Protocol.unauthorized_client); // an empty secret is not allowed for client authenticated calls. + ErrorUtilities.VerifyProtocol(MessagingUtilities.EqualsConstantTime(secret, accessRequest.ClientSecret), Protocol.incorrect_client_credentials); + + var scopedAccessRequest = accessRequest as ScopedAccessTokenRequest; + if (scopedAccessRequest != null) { + // Make sure the scope the client is requesting does not exceed the scope in the grant. + ErrorUtilities.VerifyProtocol(scopedAccessRequest.Scope.IsSubsetOf(tokenRequest.AuthorizationDescription.Scope), OAuthStrings.AccessScopeExceedsGrantScope, scopedAccessRequest.Scope, tokenRequest.AuthorizationDescription.Scope); + } + } + + // Make sure the authorization this token represents hasn't already been revoked. + ErrorUtilities.VerifyProtocol(this.AuthorizationServer.IsAuthorizationValid(tokenRequest.AuthorizationDescription), Protocol.authorization_expired); + + return MessageProtections.None; + } + + return null; + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessToken.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessToken.cs new file mode 100644 index 0000000..a502962 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessToken.cs @@ -0,0 +1,102 @@ +//----------------------------------------------------------------------- +// <copyright file="AccessToken.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Security.Cryptography; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// A short-lived token that accompanies HTTP requests to protected data to authorize the request. + /// </summary> + internal class AccessToken : AuthorizationDataBag { + /// <summary> + /// Initializes a new instance of the <see cref="AccessToken"/> class. + /// </summary> + public AccessToken() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AccessToken"/> class. + /// </summary> + /// <param name="authorization">The authorization to be described by the access token.</param> + /// <param name="lifetime">The lifetime of the access token.</param> + internal AccessToken(IAuthorizationDescription authorization, TimeSpan? lifetime) { + Requires.NotNull(authorization, "authorization"); + + this.ClientIdentifier = authorization.ClientIdentifier; + this.UtcCreationDate = authorization.UtcIssued; + this.User = authorization.User; + this.Scope.ResetContents(authorization.Scope); + this.Lifetime = lifetime; + } + + /// <summary> + /// Initializes a new instance of the <see cref="AccessToken"/> class. + /// </summary> + /// <param name="clientIdentifier">The client identifier.</param> + /// <param name="scopes">The scopes.</param> + /// <param name="username">The username of the account that authorized this token.</param> + /// <param name="lifetime">The lifetime for this access token.</param> + internal AccessToken(string clientIdentifier, IEnumerable<string> scopes, string username, TimeSpan? lifetime) { + Requires.NotNullOrEmpty(clientIdentifier, "clientIdentifier"); + + this.ClientIdentifier = clientIdentifier; + this.Scope.ResetContents(scopes); + this.User = username; + this.Lifetime = lifetime; + this.UtcCreationDate = DateTime.UtcNow; + } + + /// <summary> + /// Gets or sets the lifetime of the access token. + /// </summary> + /// <value>The lifetime.</value> + [MessagePart(Encoder = typeof(TimespanSecondsEncoder))] + internal TimeSpan? Lifetime { get; set; } + + /// <summary> + /// Creates a formatter capable of serializing/deserializing an access token. + /// </summary> + /// <param name="signingKey">The crypto service provider with the authorization server's private key used to asymmetrically sign the access token.</param> + /// <param name="encryptingKey">The crypto service provider with the resource server's public key used to encrypt the access token.</param> + /// <returns>An access token serializer.</returns> + internal static IDataBagFormatter<AccessToken> CreateFormatter(RSACryptoServiceProvider signingKey, RSACryptoServiceProvider encryptingKey) { + Contract.Requires(signingKey != null || !signingKey.PublicOnly); + Contract.Requires(encryptingKey != null); + Contract.Ensures(Contract.Result<IDataBagFormatter<AccessToken>>() != null); + + return new UriStyleMessageFormatter<AccessToken>(signingKey, encryptingKey); + } + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + protected override void EnsureValidMessage() { + base.EnsureValidMessage(); + + // Has this token expired? + if (this.Lifetime.HasValue) { + DateTime expirationDate = this.UtcCreationDate + this.Lifetime.Value; + if (expirationDate < DateTime.UtcNow) { + throw new ExpiredMessageException(expirationDate, this.ContainingMessage); + } + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessTokenBindingElement.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessTokenBindingElement.cs new file mode 100644 index 0000000..3a709b6 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AccessTokenBindingElement.cs @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------- +// <copyright file="AccessTokenBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Serializes access tokens inside an outgoing message. + /// </summary> + internal class AccessTokenBindingElement : AuthServerBindingElementBase { + /// <summary> + /// Initializes a new instance of the <see cref="AccessTokenBindingElement"/> class. + /// </summary> + internal AccessTokenBindingElement() { + } + + /// <summary> + /// Gets the protection commonly offered (if any) by this binding element. + /// </summary> + /// <value>Always <c>MessageProtections.None</c></value> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + public override MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + public override MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var directResponse = message as IDirectResponseProtocolMessage; + IAccessTokenRequest request = directResponse != null ? directResponse.OriginatingRequest as IAccessTokenRequest : null; + + var implicitGrantResponse = message as EndUserAuthorizationSuccessAccessTokenResponse; + if (implicitGrantResponse != null) { + IAuthorizationCarryingRequest tokenCarryingResponse = implicitGrantResponse; + tokenCarryingResponse.AuthorizationDescription = new AccessToken(request.ClientIdentifier, implicitGrantResponse.Scope, implicitGrantResponse.AuthorizingUsername, implicitGrantResponse.Lifetime); + + return MessageProtections.None; + } + + var accessTokenResponse = message as AccessTokenSuccessResponse; + if (accessTokenResponse != null) { + var authCarryingRequest = (IAuthorizationCarryingRequest)request; + var accessToken = new AccessToken(authCarryingRequest.AuthorizationDescription, accessTokenResponse.Lifetime); + using (var resourceServerEncryptionKey = this.AuthorizationServer.GetResourceServerEncryptionKey(request)) { + var accessTokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServer.AccessTokenSigningKey, resourceServerEncryptionKey); + accessTokenResponse.AccessToken = accessTokenFormatter.Serialize(accessToken); + } + + if (accessTokenResponse.HasRefreshToken) { + var refreshToken = new RefreshToken(authCarryingRequest.AuthorizationDescription); + var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.CryptoKeyStore); + accessTokenResponse.RefreshToken = refreshTokenFormatter.Serialize(refreshToken); + } + } + + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + public override MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + return null; + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthServerAllFlowsBindingElement.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthServerAllFlowsBindingElement.cs new file mode 100644 index 0000000..bc464e2 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthServerAllFlowsBindingElement.cs @@ -0,0 +1,83 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthServerAllFlowsBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + using DotNetOpenAuth.OAuth2.Messages; + using Messaging; + + /// <summary> + /// A binding element that should be applied for authorization server channels regardless of which flows + /// are supported. + /// </summary> + internal class AuthServerAllFlowsBindingElement : AuthServerBindingElementBase { + /// <summary> + /// Initializes a new instance of the <see cref="AuthServerAllFlowsBindingElement"/> class. + /// </summary> + internal AuthServerAllFlowsBindingElement() { + } + + /// <summary> + /// Gets the protection commonly offered (if any) by this binding element. + /// </summary> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + public override MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public override MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public override MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var authorizationRequest = message as EndUserAuthorizationRequest; + if (authorizationRequest != null) { + var client = this.AuthorizationServer.GetClientOrThrow(authorizationRequest.ClientIdentifier); + ErrorUtilities.VerifyProtocol(authorizationRequest.Callback == null || client.IsCallbackAllowed(authorizationRequest.Callback), OAuthStrings.ClientCallbackDisallowed, authorizationRequest.Callback); + ErrorUtilities.VerifyProtocol(authorizationRequest.Callback != null || client.DefaultCallback != null, OAuthStrings.NoCallback); + + return MessageProtections.None; + } + + return null; + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthServerBindingElementBase.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthServerBindingElementBase.cs new file mode 100644 index 0000000..9d3e78f --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthServerBindingElementBase.cs @@ -0,0 +1,92 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthServerBindingElementBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Messaging; + + /// <summary> + /// The base class for any authorization server channel binding element. + /// </summary> + internal abstract class AuthServerBindingElementBase : IChannelBindingElement { + /// <summary> + /// Initializes a new instance of the <see cref="AuthServerBindingElementBase"/> class. + /// </summary> + protected AuthServerBindingElementBase() + { + } + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + public Channel Channel { get; set; } + + /// <summary> + /// Gets the protection commonly offered (if any) by this binding element. + /// </summary> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + public abstract MessageProtections Protection { get; } + + /// <summary> + /// Gets the channel that this binding element belongs to. + /// </summary> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + protected OAuth2AuthorizationServerChannel OAuthChannel { + get { return (OAuth2AuthorizationServerChannel)this.Channel; } + } + + /// <summary> + /// Gets the authorization server hosting this channel. + /// </summary> + /// <value>The authorization server.</value> + protected IAuthorizationServer AuthorizationServer { + get { return this.OAuthChannel.AuthorizationServer; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public abstract MessageProtections? ProcessOutgoingMessage(IProtocolMessage message); + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public abstract MessageProtections? ProcessIncomingMessage(IProtocolMessage message); + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthorizationCode.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthorizationCode.cs new file mode 100644 index 0000000..6900b89 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthorizationCode.cs @@ -0,0 +1,100 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizationCode.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// Represents the authorization code created when a user approves authorization that + /// allows the client to request an access/refresh token. + /// </summary> + internal class AuthorizationCode : AuthorizationDataBag { + /// <summary> + /// The name of the bucket for symmetric keys used to sign authorization codes. + /// </summary> + internal const string AuthorizationCodeKeyBucket = "https://localhost/dnoa/oauth_authorization_code"; + + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizationCode"/> class. + /// </summary> + public AuthorizationCode() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizationCode"/> class. + /// </summary> + /// <param name="clientIdentifier">The client identifier.</param> + /// <param name="callback">The callback the client used to obtain authorization.</param> + /// <param name="scopes">The authorized scopes.</param> + /// <param name="username">The name on the account that authorized access.</param> + internal AuthorizationCode(string clientIdentifier, Uri callback, IEnumerable<string> scopes, string username) { + Requires.NotNullOrEmpty(clientIdentifier, "clientIdentifier"); + Requires.NotNull(callback, "callback"); + + this.ClientIdentifier = clientIdentifier; + this.CallbackHash = CalculateCallbackHash(callback); + this.Scope.ResetContents(scopes); + this.User = username; + this.UtcCreationDate = DateTime.UtcNow; + } + + /// <summary> + /// Gets or sets the hash of the callback URL. + /// </summary> + [MessagePart("cb")] + private byte[] CallbackHash { get; set; } + + /// <summary> + /// Creates a serializer/deserializer for this type. + /// </summary> + /// <param name="authorizationServer">The authorization server that will be serializing/deserializing this authorization code. Must not be null.</param> + /// <returns>A DataBag formatter.</returns> + internal static IDataBagFormatter<AuthorizationCode> CreateFormatter(IAuthorizationServer authorizationServer) { + Requires.NotNull(authorizationServer, "authorizationServer"); + Contract.Ensures(Contract.Result<IDataBagFormatter<AuthorizationCode>>() != null); + + return new UriStyleMessageFormatter<AuthorizationCode>( + authorizationServer.CryptoKeyStore, + AuthorizationCodeKeyBucket, + signed: true, + encrypted: true, + compressed: false, + maximumAge: AuthorizationCodeBindingElement.MaximumMessageAge, + decodeOnceOnly: authorizationServer.VerificationCodeNonceStore); + } + + /// <summary> + /// Verifies the the given callback URL matches the callback originally given in the authorization request. + /// </summary> + /// <param name="callback">The callback.</param> + /// <remarks> + /// This method serves to verify that the callback URL given in the original authorization request + /// and the callback URL given in the access token request match. + /// </remarks> + /// <exception cref="ProtocolException">Thrown when the callback URLs do not match.</exception> + internal void VerifyCallback(Uri callback) { + ErrorUtilities.VerifyProtocol(MessagingUtilities.AreEquivalent(this.CallbackHash, CalculateCallbackHash(callback)), Protocol.redirect_uri_mismatch); + } + + /// <summary> + /// Calculates the hash of the callback URL. + /// </summary> + /// <param name="callback">The callback whose hash should be calculated.</param> + /// <returns> + /// A base64 encoding of the hash of the URL. + /// </returns> + private static byte[] CalculateCallbackHash(Uri callback) { + using (var hasher = new SHA256Managed()) { + return hasher.ComputeHash(Encoding.UTF8.GetBytes(callback.AbsoluteUri)); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthorizationCodeBindingElement.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthorizationCodeBindingElement.cs new file mode 100644 index 0000000..b0e6203 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthorizationCodeBindingElement.cs @@ -0,0 +1,101 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizationCodeBindingElement.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Messages; + using Messaging; + using Messaging.Bindings; + + /// <summary> + /// A binding element for OAuth 2.0 authorization servers that create/verify + /// issued authorization codes as part of obtaining access/refresh tokens. + /// </summary> + internal class AuthorizationCodeBindingElement : AuthServerBindingElementBase { + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizationCodeBindingElement"/> class. + /// </summary> + internal AuthorizationCodeBindingElement() { + } + + /// <summary> + /// Gets the protection commonly offered (if any) by this binding element. + /// </summary> + /// <value>Always <c>MessageProtections.None</c></value> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + public override MessageProtections Protection { + get { return MessageProtections.None; } + } + + /// <summary> + /// Gets the maximum message age from the standard expiration binding element. + /// </summary> + /// <value>This interval need not account for clock skew because it is only compared within a single authorization server or farm of servers.</value> + internal static TimeSpan MaximumMessageAge { + get { return Configuration.DotNetOpenAuthSection.Messaging.MaximumMessageLifetimeNoSkew; } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public override MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { + var response = message as EndUserAuthorizationSuccessAuthCodeResponse; + if (response != null) { + var directResponse = (IDirectResponseProtocolMessage)response; + var request = (EndUserAuthorizationRequest)directResponse.OriginatingRequest; + IAuthorizationCarryingRequest tokenCarryingResponse = response; + tokenCarryingResponse.AuthorizationDescription = new AuthorizationCode(request.ClientIdentifier, request.Callback, response.Scope, response.AuthorizingUsername); + + return MessageProtections.None; + } + + return null; + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + public override MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var request = message as AccessTokenAuthorizationCodeRequest; + if (request != null) { + IAuthorizationCarryingRequest tokenRequest = request; + ((AuthorizationCode)tokenRequest.AuthorizationDescription).VerifyCallback(request.Callback); + + return MessageProtections.None; + } + + return null; + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthorizationDataBag.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthorizationDataBag.cs new file mode 100644 index 0000000..8f1d983 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/AuthorizationDataBag.cs @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizationDataBag.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + + using DotNetOpenAuth.Messaging; + + /// <summary> + /// A data bag that stores authorization data. + /// </summary> + internal abstract class AuthorizationDataBag : DataBag, IAuthorizationDescription { + /// <summary> + /// Initializes a new instance of the <see cref="AuthorizationDataBag"/> class. + /// </summary> + protected AuthorizationDataBag() { + this.Scope = new HashSet<string>(OAuthUtilities.ScopeStringComparer); + } + + /// <summary> + /// Gets or sets the identifier of the client authorized to access protected data. + /// </summary> + /// <value></value> + [MessagePart] + public string ClientIdentifier { get; set; } + + /// <summary> + /// Gets the date this authorization was established or the token was issued. + /// </summary> + /// <value>A date/time expressed in UTC.</value> + public DateTime UtcIssued { + get { return this.UtcCreationDate; } + } + + /// <summary> + /// Gets or sets the name on the account whose data on the resource server is accessible using this authorization. + /// </summary> + [MessagePart] + public string User { get; set; } + + /// <summary> + /// Gets the scope of operations the client is allowed to invoke. + /// </summary> + [MessagePart(Encoder = typeof(ScopeEncoder))] + public HashSet<string> Scope { get; private set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/EndUserAuthorizationResponseTypeEncoder.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/EndUserAuthorizationResponseTypeEncoder.cs new file mode 100644 index 0000000..139025d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/EndUserAuthorizationResponseTypeEncoder.cs @@ -0,0 +1,66 @@ +//----------------------------------------------------------------------- +// <copyright file="EndUserAuthorizationResponseTypeEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Encodes/decodes the OAuth 2.0 response_type argument. + /// </summary> + internal class EndUserAuthorizationResponseTypeEncoder : IMessagePartEncoder { + /// <summary> + /// Initializes a new instance of the <see cref="EndUserAuthorizationResponseTypeEncoder"/> class. + /// </summary> + public EndUserAuthorizationResponseTypeEncoder() { + } + + #region IMessagePartEncoder Members + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + var responseType = (EndUserAuthorizationResponseType)value; + switch (responseType) + { + case EndUserAuthorizationResponseType.AccessToken: + return Protocol.ResponseTypes.Token; + case EndUserAuthorizationResponseType.AuthorizationCode: + return Protocol.ResponseTypes.Code; + default: + throw ErrorUtilities.ThrowFormat(MessagingStrings.UnexpectedMessagePartValue, Protocol.response_type, value); + } + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + switch (value) { + case Protocol.ResponseTypes.Token: + return EndUserAuthorizationResponseType.AccessToken; + case Protocol.ResponseTypes.Code: + return EndUserAuthorizationResponseType.AuthorizationCode; + default: + throw ErrorUtilities.ThrowFormat(MessagingStrings.UnexpectedMessagePartValue, Protocol.response_type, value); + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/GrantTypeEncoder.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/GrantTypeEncoder.cs new file mode 100644 index 0000000..78ed975 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/GrantTypeEncoder.cs @@ -0,0 +1,78 @@ +//----------------------------------------------------------------------- +// <copyright file="GrantTypeEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Encodes/decodes the OAuth 2.0 grant_type argument. + /// </summary> + internal class GrantTypeEncoder : IMessagePartEncoder { + /// <summary> + /// Initializes a new instance of the <see cref="GrantTypeEncoder"/> class. + /// </summary> + public GrantTypeEncoder() { + } + + #region IMessagePartEncoder Members + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + var responseType = (GrantType)value; + switch (responseType) + { + case GrantType.ClientCredentials: + return Protocol.GrantTypes.ClientCredentials; + case GrantType.AuthorizationCode: + return Protocol.GrantTypes.AuthorizationCode; + case GrantType.RefreshToken: + return Protocol.GrantTypes.RefreshToken; + case GrantType.Password: + return Protocol.GrantTypes.Password; + case GrantType.Assertion: + return Protocol.GrantTypes.Assertion; + default: + throw ErrorUtilities.ThrowFormat(MessagingStrings.UnexpectedMessagePartValue, Protocol.grant_type, value); + } + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + switch (value) { + case Protocol.GrantTypes.ClientCredentials: + return GrantType.ClientCredentials; + case Protocol.GrantTypes.Assertion: + return GrantType.Assertion; + case Protocol.GrantTypes.Password: + return GrantType.Password; + case Protocol.GrantTypes.RefreshToken: + return GrantType.RefreshToken; + case Protocol.GrantTypes.AuthorizationCode: + return GrantType.AuthorizationCode; + default: + throw ErrorUtilities.ThrowFormat(MessagingStrings.UnexpectedMessagePartValue, Protocol.grant_type, value); + } + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/IAuthorizationCarryingRequest.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/IAuthorizationCarryingRequest.cs new file mode 100644 index 0000000..e450131 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/IAuthorizationCarryingRequest.cs @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------- +// <copyright file="IAuthorizationCarryingRequest.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System.Security.Cryptography; + + using Messaging; + + /// <summary> + /// The various types of tokens created by the authorization server. + /// </summary> + internal enum CodeOrTokenType { + /// <summary> + /// The code issued to the client after the user has approved authorization. + /// </summary> + AuthorizationCode, + + /// <summary> + /// The long-lived token issued to the client that enables it to obtain + /// short-lived access tokens later. + /// </summary> + RefreshToken, + + /// <summary> + /// A (typically) short-lived token. + /// </summary> + AccessToken, + } + + /// <summary> + /// A message that carries some kind of token from the client to the authorization or resource server. + /// </summary> + internal interface IAuthorizationCarryingRequest : IDirectedProtocolMessage { + /// <summary> + /// Gets or sets the verification code or refresh/access token. + /// </summary> + /// <value>The code or token.</value> + string CodeOrToken { get; set; } + + /// <summary> + /// Gets the type of the code or token. + /// </summary> + /// <value>The type of the code or token.</value> + CodeOrTokenType CodeOrTokenType { get; } + + /// <summary> + /// Gets or sets the authorization that the token describes. + /// </summary> + IAuthorizationDescription AuthorizationDescription { get; set; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/IAuthorizationDescription.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/IAuthorizationDescription.cs new file mode 100644 index 0000000..2b3a9ce --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/IAuthorizationDescription.cs @@ -0,0 +1,90 @@ +//----------------------------------------------------------------------- +// <copyright file="IAuthorizationDescription.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + + /// <summary> + /// Describes a delegated authorization between a resource server, a client, and a user. + /// </summary> + [ContractClass(typeof(IAuthorizationDescriptionContract))] + public interface IAuthorizationDescription { + /// <summary> + /// Gets the identifier of the client authorized to access protected data. + /// </summary> + string ClientIdentifier { get; } + + /// <summary> + /// Gets the date this authorization was established or the token was issued. + /// </summary> + /// <value>A date/time expressed in UTC.</value> + DateTime UtcIssued { get; } + + /// <summary> + /// Gets the name on the account whose data on the resource server is accessible using this authorization. + /// </summary> + string User { get; } + + /// <summary> + /// Gets the scope of operations the client is allowed to invoke. + /// </summary> + HashSet<string> Scope { get; } + } + + /// <summary> + /// Code contract for the <see cref="IAuthorizationDescription"/> interface. + /// </summary> + [ContractClassFor(typeof(IAuthorizationDescription))] + internal abstract class IAuthorizationDescriptionContract : IAuthorizationDescription { + /// <summary> + /// Prevents a default instance of the <see cref="IAuthorizationDescriptionContract"/> class from being created. + /// </summary> + private IAuthorizationDescriptionContract() { + } + + /// <summary> + /// Gets the identifier of the client authorized to access protected data. + /// </summary> + string IAuthorizationDescription.ClientIdentifier { + get { + Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); + throw new NotImplementedException(); + } + } + + /// <summary> + /// Gets the date this authorization was established or the token was issued. + /// </summary> + /// <value>A date/time expressed in UTC.</value> + DateTime IAuthorizationDescription.UtcIssued { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets the name on the account whose data on the resource server is accessible using this authorization. + /// </summary> + string IAuthorizationDescription.User { + get { + Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); + throw new NotImplementedException(); + } + } + + /// <summary> + /// Gets the scope of operations the client is allowed to invoke. + /// </summary> + HashSet<string> IAuthorizationDescription.Scope { + get { + Contract.Ensures(Contract.Result<HashSet<string>>() != null); + throw new NotImplementedException(); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs new file mode 100644 index 0000000..0c31023 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2AuthorizationServerChannel.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Net.Mime; + using System.Web; + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The channel for the OAuth protocol. + /// </summary> + internal class OAuth2AuthorizationServerChannel : OAuth2ChannelBase { + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2AuthorizationServerChannel"/> class. + /// </summary> + /// <param name="authorizationServer">The authorization server.</param> + protected internal OAuth2AuthorizationServerChannel(IAuthorizationServer authorizationServer) + : base(InitializeBindingElements(authorizationServer)) { + Requires.NotNull(authorizationServer, "authorizationServer"); + this.AuthorizationServer = authorizationServer; + } + + /// <summary> + /// Gets the authorization server. + /// </summary> + /// <value>The authorization server.</value> + public IAuthorizationServer AuthorizationServer { get; private set; } + + /// <summary> + /// Gets the protocol message that may be in the given HTTP response. + /// </summary> + /// <param name="response">The response that is anticipated to contain an protocol message.</param> + /// <returns> + /// The deserialized message parts, if found. Null otherwise. + /// </returns> + /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception> + protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) { + throw new NotImplementedException(); + } + + /// <summary> + /// Queues a message for sending in the response stream. + /// </summary> + /// <param name="response">The message to send as a response.</param> + /// <returns> + /// The pending user agent redirect based message to be sent as an HttpResponse. + /// </returns> + /// <remarks> + /// This method implements spec OAuth V1.0 section 5.3. + /// </remarks> + protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { + var webResponse = new OutgoingWebResponse(); + string json = this.SerializeAsJson(response); + webResponse.SetResponse(json, new ContentType(JsonEncoded)); + return webResponse; + } + + /// <summary> + /// Gets the protocol message that may be embedded in the given HTTP request. + /// </summary> + /// <param name="request">The request to search for an embedded message.</param> + /// <returns> + /// The deserialized message, if one is found. Null otherwise. + /// </returns> + protected override IDirectedProtocolMessage ReadFromRequestCore(HttpRequestInfo request) { + if (!string.IsNullOrEmpty(request.Url.Fragment)) { + var fields = HttpUtility.ParseQueryString(request.Url.Fragment.Substring(1)).ToDictionary(); + + MessageReceivingEndpoint recipient; + try { + recipient = request.GetRecipient(); + } catch (ArgumentException ex) { + Logger.Messaging.WarnFormat("Unrecognized HTTP request: " + ex.ToString()); + return null; + } + + return (IDirectedProtocolMessage)this.Receive(fields, recipient); + } + + return base.ReadFromRequestCore(request); + } + + /// <summary> + /// Initializes the binding elements for the OAuth channel. + /// </summary> + /// <param name="authorizationServer">The authorization server.</param> + /// <returns> + /// An array of binding elements used to initialize the channel. + /// </returns> + private static IChannelBindingElement[] InitializeBindingElements(IAuthorizationServer authorizationServer) { + Requires.NotNull(authorizationServer, "authorizationServer"); + var bindingElements = new List<IChannelBindingElement>(); + + bindingElements.Add(new AuthServerAllFlowsBindingElement()); + bindingElements.Add(new AuthorizationCodeBindingElement()); + bindingElements.Add(new AccessTokenBindingElement()); + bindingElements.Add(new AccessRequestBindingElement()); + + return bindingElements.ToArray(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2ChannelBase.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2ChannelBase.cs new file mode 100644 index 0000000..a646f51 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2ChannelBase.cs @@ -0,0 +1,69 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2ChannelBase.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// The base messaging channel used by OAuth 2.0 parties. + /// </summary> + internal abstract class OAuth2ChannelBase : StandardMessageFactoryChannel { + /// <summary> + /// The messages receivable by this channel. + /// </summary> + private static readonly Type[] MessageTypes = new Type[] { + typeof(AccessTokenRefreshRequest), + typeof(AccessTokenAuthorizationCodeRequest), + typeof(AccessTokenResourceOwnerPasswordCredentialsRequest), + typeof(AccessTokenClientCredentialsRequest), + typeof(AccessTokenSuccessResponse), + typeof(AccessTokenFailedResponse), + typeof(EndUserAuthorizationRequest), + typeof(EndUserAuthorizationSuccessAuthCodeResponse), + typeof(EndUserAuthorizationSuccessAccessTokenResponse), + typeof(EndUserAuthorizationFailedResponse), + typeof(UnauthorizedResponse), + }; + + /// <summary> + /// The protocol versions supported by this channel. + /// </summary> + private static readonly Version[] Versions = Protocol.AllVersions.Select(v => v.Version).ToArray(); + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2ChannelBase"/> class. + /// </summary> + /// <param name="channelBindingElements">The channel binding elements.</param> + internal OAuth2ChannelBase(params IChannelBindingElement[] channelBindingElements) + : base(MessageTypes, Versions, channelBindingElements) { + } + + /// <summary> + /// Allows preprocessing and validation of message data before an appropriate message type is + /// selected or deserialized. + /// </summary> + /// <param name="fields">The received message data.</param> + protected override void FilterReceivedFields(IDictionary<string, string> fields) { + base.FilterReceivedFields(fields); + + // Apply the OAuth 2.0 section 2.1 requirement: + // Parameters sent without a value MUST be treated as if they were omitted from the request. + // The authorization server SHOULD ignore unrecognized request parameters. + var emptyKeys = from pair in fields + where String.IsNullOrEmpty(pair.Value) + select pair.Key; + foreach (string emptyKey in emptyKeys.ToList()) { + fields.Remove(emptyKey); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2ClientChannel.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2ClientChannel.cs new file mode 100644 index 0000000..e4a9afd --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2ClientChannel.cs @@ -0,0 +1,122 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2ClientChannel.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Diagnostics.Contracts; + using System.Net; + using System.Web; + + using DotNetOpenAuth.Messaging; + + /// <summary> + /// The messaging channel used by OAuth 2.0 Clients. + /// </summary> + internal class OAuth2ClientChannel : OAuth2ChannelBase { + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2ClientChannel"/> class. + /// </summary> + internal OAuth2ClientChannel() { + } + + /// <summary> + /// Prepares an HTTP request that carries a given message. + /// </summary> + /// <param name="request">The message to send.</param> + /// <returns> + /// The <see cref="HttpWebRequest"/> prepared to send the request. + /// </returns> + /// <remarks> + /// This method must be overridden by a derived class, unless the <see cref="Channel.RequestCore"/> method + /// is overridden and does not require this method. + /// </remarks> + protected override HttpWebRequest CreateHttpRequest(IDirectedProtocolMessage request) { + HttpWebRequest httpRequest; + if ((request.HttpMethods & HttpDeliveryMethods.GetRequest) != 0) { + httpRequest = InitializeRequestAsGet(request); + } else if ((request.HttpMethods & HttpDeliveryMethods.PostRequest) != 0) { + httpRequest = InitializeRequestAsPost(request); + } else { + throw new NotSupportedException(); + } + + return httpRequest; + } + + /// <summary> + /// Gets the protocol message that may be in the given HTTP response. + /// </summary> + /// <param name="response">The response that is anticipated to contain an protocol message.</param> + /// <returns> + /// The deserialized message parts, if found. Null otherwise. + /// </returns> + /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception> + protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) { + // The spec says direct responses should be JSON objects, but Facebook uses HttpFormUrlEncoded instead, calling it text/plain + // Others return text/javascript. Again bad. + string body = response.GetResponseReader().ReadToEnd(); + if (response.ContentType.MediaType == JsonEncoded || response.ContentType.MediaType == JsonTextEncoded) { + return this.DeserializeFromJson(body); + } else if (response.ContentType.MediaType == HttpFormUrlEncoded || response.ContentType.MediaType == PlainTextEncoded) { + return HttpUtility.ParseQueryString(body).ToDictionary(); + } else { + throw ErrorUtilities.ThrowProtocol("Unexpected response Content-Type {0}", response.ContentType.MediaType); + } + } + + /// <summary> + /// Gets the protocol message that may be embedded in the given HTTP request. + /// </summary> + /// <param name="request">The request to search for an embedded message.</param> + /// <returns> + /// The deserialized message, if one is found. Null otherwise. + /// </returns> + protected override IDirectedProtocolMessage ReadFromRequestCore(HttpRequestInfo request) { + Logger.Channel.DebugFormat("Incoming HTTP request: {0} {1}", request.HttpMethod, request.UrlBeforeRewriting.AbsoluteUri); + + var fields = request.QueryStringBeforeRewriting.ToDictionary(); + + // Also read parameters from the fragment, if it's available. + // Typically the fragment is not available because the browser doesn't send it to a web server + // but this request may have been fabricated by an installed desktop app, in which case + // the fragment is available. + string fragment = request.UrlBeforeRewriting.Fragment; + if (!string.IsNullOrEmpty(fragment)) { + foreach (var pair in HttpUtility.ParseQueryString(fragment.Substring(1)).ToDictionary()) { + fields.Add(pair.Key, pair.Value); + } + } + + MessageReceivingEndpoint recipient; + try { + recipient = request.GetRecipient(); + } catch (ArgumentException ex) { + Logger.Messaging.WarnFormat("Unrecognized HTTP request: ", ex); + return null; + } + + return (IDirectedProtocolMessage)this.Receive(fields, recipient); + } + + /// <summary> + /// Queues a message for sending in the response stream where the fields + /// are sent in the response stream in querystring style. + /// </summary> + /// <param name="response">The message to send as a response.</param> + /// <returns> + /// The pending user agent redirect based message to be sent as an HttpResponse. + /// </returns> + /// <remarks> + /// This method implements spec OAuth V1.0 section 5.3. + /// </remarks> + protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { + // Clients don't ever send direct responses. + throw new NotImplementedException(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs new file mode 100644 index 0000000..97a70c8 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/OAuth2ResourceServerChannel.cs @@ -0,0 +1,153 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2ResourceServerChannel.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Net; + using System.Net.Mime; + using System.Text; + using System.Web; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// The channel for the OAuth protocol. + /// </summary> + internal class OAuth2ResourceServerChannel : StandardMessageFactoryChannel { + /// <summary> + /// The messages receivable by this channel. + /// </summary> + private static readonly Type[] MessageTypes = new Type[] { + typeof(Messages.AccessProtectedResourceRequest), + }; + + /// <summary> + /// The protocol versions supported by this channel. + /// </summary> + private static readonly Version[] Versions = Protocol.AllVersions.Select(v => v.Version).ToArray(); + + /// <summary> + /// Initializes a new instance of the <see cref="OAuth2ResourceServerChannel"/> class. + /// </summary> + protected internal OAuth2ResourceServerChannel() + : base(MessageTypes, Versions) { + // TODO: add signing (authenticated request) binding element. + } + + /// <summary> + /// Gets the protocol message that may be embedded in the given HTTP request. + /// </summary> + /// <param name="request">The request to search for an embedded message.</param> + /// <returns> + /// The deserialized message, if one is found. Null otherwise. + /// </returns> + protected override IDirectedProtocolMessage ReadFromRequestCore(HttpRequestInfo request) { + var fields = new Dictionary<string, string>(); + string accessToken; + if ((accessToken = SearchForBearerAccessTokenInRequest(request)) != null) { + fields["token_type"] = Protocol.AccessTokenTypes.Bearer; + fields["access_token"] = accessToken; + } + + if (fields.Count > 0) { + MessageReceivingEndpoint recipient; + try { + recipient = request.GetRecipient(); + } catch (ArgumentException ex) { + Logger.OAuth.WarnFormat("Unrecognized HTTP request: " + ex.ToString()); + return null; + } + + // Deserialize the message using all the data we've collected. + var message = (IDirectedProtocolMessage)this.Receive(fields, recipient); + return message; + } + + return null; + } + + /// <summary> + /// Gets the protocol message that may be in the given HTTP response. + /// </summary> + /// <param name="response">The response that is anticipated to contain an protocol message.</param> + /// <returns> + /// The deserialized message parts, if found. Null otherwise. + /// </returns> + /// <exception cref="ProtocolException">Thrown when the response is not valid.</exception> + protected override IDictionary<string, string> ReadFromResponseCore(IncomingWebResponse response) { + // We never expect resource servers to send out direct requests, + // and therefore won't have direct responses. + throw new NotImplementedException(); + } + + /// <summary> + /// Queues a message for sending in the response stream where the fields + /// are sent in the response stream in querystring style. + /// </summary> + /// <param name="response">The message to send as a response.</param> + /// <returns> + /// The pending user agent redirect based message to be sent as an HttpResponse. + /// </returns> + /// <remarks> + /// This method implements spec OAuth V1.0 section 5.3. + /// </remarks> + protected override OutgoingWebResponse PrepareDirectResponse(IProtocolMessage response) { + var webResponse = new OutgoingWebResponse(); + + // The only direct response from a resource server is a 401 Unauthorized error. + var unauthorizedResponse = response as UnauthorizedResponse; + ErrorUtilities.VerifyInternal(unauthorizedResponse != null, "Only unauthorized responses are expected."); + + // First initialize based on the specifics within the message. + var httpResponse = response as IHttpDirectResponse; + webResponse.Status = httpResponse != null ? httpResponse.HttpStatusCode : HttpStatusCode.Unauthorized; + foreach (string headerName in httpResponse.Headers) { + webResponse.Headers.Add(headerName, httpResponse.Headers[headerName]); + } + + // Now serialize all the message parts into the WWW-Authenticate header. + var fields = this.MessageDescriptions.GetAccessor(response); + webResponse.Headers[HttpResponseHeader.WwwAuthenticate] = MessagingUtilities.AssembleAuthorizationHeader(Protocol.BearerHttpAuthorizationScheme, fields); + return webResponse; + } + + /// <summary> + /// Searches for a bearer access token in the request. + /// </summary> + /// <param name="request">The request.</param> + /// <returns>The bearer access token, if one exists. Otherwise <c>null</c>.</returns> + private static string SearchForBearerAccessTokenInRequest(HttpRequestInfo request) { + Requires.NotNull(request, "request"); + + // First search the authorization header. + string authorizationHeader = request.Headers[HttpRequestHeader.Authorization]; + if (!string.IsNullOrEmpty(authorizationHeader) && authorizationHeader.StartsWith(Protocol.BearerHttpAuthorizationSchemeWithTrailingSpace, StringComparison.OrdinalIgnoreCase)) { + return authorizationHeader.Substring(Protocol.BearerHttpAuthorizationSchemeWithTrailingSpace.Length); + } + + // Failing that, scan the entity + if (!string.IsNullOrEmpty(request.Headers[HttpRequestHeader.ContentType])) { + var contentType = new ContentType(request.Headers[HttpRequestHeader.ContentType]); + if (string.Equals(contentType.MediaType, HttpFormUrlEncoded, StringComparison.Ordinal)) { + if (request.Form[Protocol.BearerTokenEncodedUrlParameterName] != null) { + return request.Form[Protocol.BearerTokenEncodedUrlParameterName]; + } + } + } + + // Finally, check the least desirable location: the query string + if (!String.IsNullOrEmpty(request.QueryStringBeforeRewriting[Protocol.BearerTokenEncodedUrlParameterName])) { + return request.QueryStringBeforeRewriting[Protocol.BearerTokenEncodedUrlParameterName]; + } + + return null; + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/RefreshToken.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/RefreshToken.cs new file mode 100644 index 0000000..ee41957 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/RefreshToken.cs @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------- +// <copyright file="RefreshToken.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Diagnostics.Contracts; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + + /// <summary> + /// The refresh token issued to a client by an authorization server that allows the client + /// to periodically obtain new short-lived access tokens. + /// </summary> + internal class RefreshToken : AuthorizationDataBag { + /// <summary> + /// The name of the bucket for symmetric keys used to sign refresh tokens. + /// </summary> + internal const string RefreshTokenKeyBucket = "https://localhost/dnoa/oauth_refresh_token"; + + /// <summary> + /// Initializes a new instance of the <see cref="RefreshToken"/> class. + /// </summary> + public RefreshToken() { + } + + /// <summary> + /// Initializes a new instance of the <see cref="RefreshToken"/> class. + /// </summary> + /// <param name="authorization">The authorization this refresh token should describe.</param> + internal RefreshToken(IAuthorizationDescription authorization) { + Requires.NotNull(authorization, "authorization"); + + this.ClientIdentifier = authorization.ClientIdentifier; + this.UtcCreationDate = authorization.UtcIssued; + this.User = authorization.User; + this.Scope.ResetContents(authorization.Scope); + } + + /// <summary> + /// Creates a formatter capable of serializing/deserializing a refresh token. + /// </summary> + /// <param name="cryptoKeyStore">The crypto key store.</param> + /// <returns> + /// A DataBag formatter. Never null. + /// </returns> + internal static IDataBagFormatter<RefreshToken> CreateFormatter(ICryptoKeyStore cryptoKeyStore) { + Requires.NotNull(cryptoKeyStore, "cryptoKeyStore"); + Contract.Ensures(Contract.Result<IDataBagFormatter<RefreshToken>>() != null); + + return new UriStyleMessageFormatter<RefreshToken>(cryptoKeyStore, RefreshTokenKeyBucket, signed: true, encrypted: true); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/ScopeEncoder.cs b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/ScopeEncoder.cs new file mode 100644 index 0000000..7ae5fbf --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2/OAuth2/ChannelElements/ScopeEncoder.cs @@ -0,0 +1,51 @@ +//----------------------------------------------------------------------- +// <copyright file="ScopeEncoder.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Reflection; + + /// <summary> + /// Encodes or decodes a set of scopes into the OAuth 2.0 scope message part. + /// </summary> + internal class ScopeEncoder : IMessagePartEncoder { + /// <summary> + /// Initializes a new instance of the <see cref="ScopeEncoder"/> class. + /// </summary> + public ScopeEncoder() { + } + + /// <summary> + /// Encodes the specified value. + /// </summary> + /// <param name="value">The value. Guaranteed to never be null.</param> + /// <returns> + /// The <paramref name="value"/> in string form, ready for message transport. + /// </returns> + public string Encode(object value) { + var scopes = (IEnumerable<string>)value; + ErrorUtilities.VerifyProtocol(!scopes.Any(scope => scope.Contains(" ")), OAuthStrings.ScopesMayNotContainSpaces); + return (scopes != null && scopes.Any()) ? string.Join(" ", scopes.ToArray()) : null; + } + + /// <summary> + /// Decodes the specified value. + /// </summary> + /// <param name="value">The string value carried by the transport. Guaranteed to never be null, although it may be empty.</param> + /// <returns> + /// The deserialized form of the given string. + /// </returns> + /// <exception cref="FormatException">Thrown when the string value given cannot be decoded into the required object type.</exception> + public object Decode(string value) { + return OAuthUtilities.SplitScopes(value); + } + } +} |