diff options
Diffstat (limited to 'src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements')
8 files changed, 772 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AccessTokenBindingElement.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AccessTokenBindingElement.cs new file mode 100644 index 0000000..9d7e8f2 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AccessTokenBindingElement.cs @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------- +// <copyright file="AccessTokenBindingElement.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics; + 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; + var request = directResponse != null ? directResponse.OriginatingRequest as IAccessTokenRequestInternal : null; + var authCarryingRequest = request as IAuthorizationCarryingRequest; + var accessTokenResponse = message as IAccessTokenIssuingResponse; + var implicitGrantResponse = message as EndUserAuthorizationSuccessAccessTokenResponse; + + if (request != null) { + request.AccessTokenCreationParameters = this.AuthorizationServer.GetAccessTokenParameters(request); + ErrorUtilities.VerifyHost(request.AccessTokenCreationParameters != null, "IAuthorizationServer.GetAccessTokenParameters must not return null."); + + if (accessTokenResponse != null) { + accessTokenResponse.Lifetime = request.AccessTokenCreationParameters.AccessTokenLifetime; + } + } + + if (authCarryingRequest != null) { + ErrorUtilities.VerifyInternal(request != null, MessagingStrings.UnexpectedMessageReceived, typeof(IAccessTokenRequestInternal), request.GetType()); + accessTokenResponse.AuthorizationDescription = new AccessToken(authCarryingRequest.AuthorizationDescription, accessTokenResponse.Lifetime); + } else if (implicitGrantResponse != null) { + IAccessTokenCarryingRequest tokenCarryingResponse = implicitGrantResponse; + accessTokenResponse.AuthorizationDescription = new AccessToken( + request.ClientIdentifier, + implicitGrantResponse.Scope, + implicitGrantResponse.AuthorizingUsername, + implicitGrantResponse.Lifetime); + } + + 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.AuthorizationServer/OAuth2/ChannelElements/AuthServerBindingElementBase.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AuthServerBindingElementBase.cs new file mode 100644 index 0000000..49f820d --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AuthServerBindingElementBase.cs @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthServerBindingElementBase.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. 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 authorization server hosting this channel. + /// </summary> + /// <value>The authorization server.</value> + protected IAuthorizationServer AuthorizationServer { + get { return ((IOAuth2ChannelWithAuthorizationServer)this.Channel).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.AuthorizationServer/OAuth2/ChannelElements/AuthorizationCode.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AuthorizationCode.cs new file mode 100644 index 0000000..f5f25b2 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/AuthorizationCode.cs @@ -0,0 +1,118 @@ +//----------------------------------------------------------------------- +// <copyright file="AuthorizationCode.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + 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, if one was explicitly included in the request.</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"); + + this.ClientIdentifier = clientIdentifier; + this.CallbackHash = CalculateCallbackHash(callback); + this.Scope.ResetContents(scopes); + this.User = username; + this.UtcCreationDate = DateTime.UtcNow; + } + + /// <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> + /// 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); + + var cryptoStore = authorizationServer.CryptoKeyStore; + ErrorUtilities.VerifyHost(cryptoStore != null, OAuthStrings.ResultShouldNotBeNull, authorizationServer.GetType(), "CryptoKeyStore"); + + return new UriStyleMessageFormatter<AuthorizationCode>( + cryptoStore, + AuthorizationCodeKeyBucket, + signed: true, + encrypted: true, + compressed: false, + maximumAge: 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> + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "redirecturimismatch", Justification = "Protocol requirement")] + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "DotNetOpenAuth.Messaging.ErrorUtilities.VerifyProtocol(System.Boolean,System.String,System.Object[])", Justification = "Protocol requirement")] + internal void VerifyCallback(Uri callback) { + ErrorUtilities.VerifyProtocol(MessagingUtilities.AreEquivalentConstantTime(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> + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "False positive.")] + private static byte[] CalculateCallbackHash(Uri callback) { + if (callback == null) { + return null; + } + + using (var hasher = new SHA256Managed()) { + return hasher.ComputeHash(Encoding.UTF8.GetBytes(callback.AbsoluteUri)); + } + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/IOAuth2ChannelWithAuthorizationServer.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/IOAuth2ChannelWithAuthorizationServer.cs new file mode 100644 index 0000000..5fc73ce --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/IOAuth2ChannelWithAuthorizationServer.cs @@ -0,0 +1,19 @@ +//----------------------------------------------------------------------- +// <copyright file="IOAuth2ChannelWithAuthorizationServer.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + /// <summary> + /// An interface on an OAuth 2 Authorization Server channel + /// to expose the host provided authorization server object. + /// </summary> + internal interface IOAuth2ChannelWithAuthorizationServer { + /// <summary> + /// Gets the authorization server. + /// </summary> + /// <value>The authorization server.</value> + IAuthorizationServer AuthorizationServer { get; } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs new file mode 100644 index 0000000..43ce243 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/MessageValidationBindingElement.cs @@ -0,0 +1,160 @@ +//----------------------------------------------------------------------- +// <copyright file="MessageValidationBindingElement.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. 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 guard for all messages to or from an Authorization Server to ensure that they are well formed, + /// have valid secrets, callback URIs, etc. + /// </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 MessageValidationBindingElement : AuthServerBindingElementBase { + /// <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) { + 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) { + bool applied = false; + + // Check that the client secret is correct for client authenticated messages. + var clientCredentialOnly = message as AccessTokenClientCredentialsRequest; + var authenticatedClientRequest = message as AuthenticatedClientRequestBase; + if (authenticatedClientRequest != null) { + var client = this.AuthorizationServer.GetClientOrThrow(authenticatedClientRequest.ClientIdentifier); + string secret = client.Secret; + AuthServerUtilities.TokenEndpointVerify(!string.IsNullOrEmpty(secret), Protocol.AccessTokenRequestErrorCodes.UnauthorizedClient); // an empty secret is not allowed for client authenticated calls. + AuthServerUtilities.TokenEndpointVerify(MessagingUtilities.EqualsConstantTime(secret, authenticatedClientRequest.ClientSecret), Protocol.AccessTokenRequestErrorCodes.InvalidClient, AuthServerStrings.ClientSecretMismatch); + + if (clientCredentialOnly != null) { + clientCredentialOnly.CredentialsValidated = true; + } + + applied = true; + } + + // Check that any resource owner password credential is correct. + var resourceOwnerPasswordCarrier = message as AccessTokenResourceOwnerPasswordCredentialsRequest; + if (resourceOwnerPasswordCarrier != null) { + try { + if (this.AuthorizationServer.IsResourceOwnerCredentialValid(resourceOwnerPasswordCarrier.UserName, resourceOwnerPasswordCarrier.Password)) { + resourceOwnerPasswordCarrier.CredentialsValidated = true; + } else { + Logger.OAuth.ErrorFormat( + "Resource owner password credential for user \"{0}\" rejected by authorization server host.", + resourceOwnerPasswordCarrier.UserName); + throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.InvalidGrant, AuthServerStrings.InvalidResourceOwnerPasswordCredential); + } + } catch (NotSupportedException) { + throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.UnsupportedGrantType); + } catch (NotImplementedException) { + throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.UnsupportedGrantType); + } + } + + // Check that authorization requests come with an acceptable callback URI. + 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); + applied = true; + } + + // Check that the callback URI in a direct message from the client matches the one in the indirect message received earlier. + var request = message as AccessTokenAuthorizationCodeRequestAS; + if (request != null) { + IAuthorizationCodeCarryingRequest tokenRequest = request; + tokenRequest.AuthorizationDescription.VerifyCallback(request.Callback); + applied = true; + } + + var authCarrier = message as IAuthorizationCarryingRequest; + if (authCarrier != null) { + var accessRequest = authCarrier as AccessTokenRequestBase; + if (accessRequest != null) { + // Make sure the client sending us this token is the client we issued the token to. + AuthServerUtilities.TokenEndpointVerify(string.Equals(accessRequest.ClientIdentifier, authCarrier.AuthorizationDescription.ClientIdentifier, StringComparison.Ordinal), Protocol.AccessTokenRequestErrorCodes.InvalidClient); + + var scopedAccessRequest = accessRequest as ScopedAccessTokenRequest; + if (scopedAccessRequest != null) { + // Make sure the scope the client is requesting does not exceed the scope in the grant. + if (!scopedAccessRequest.Scope.IsSubsetOf(authCarrier.AuthorizationDescription.Scope)) { + Logger.OAuth.ErrorFormat("The requested access scope (\"{0}\") exceeds the grant scope (\"{1}\").", scopedAccessRequest.Scope, authCarrier.AuthorizationDescription.Scope); + throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.InvalidScope, AuthServerStrings.AccessScopeExceedsGrantScope); + } + } + } + + // Make sure the authorization this token represents hasn't already been revoked. + if (!this.AuthorizationServer.IsAuthorizationValid(authCarrier.AuthorizationDescription)) { + Logger.OAuth.Error("Rejecting access token request because the IAuthorizationServer.IsAuthorizationValid method returned false."); + throw new TokenEndpointProtocolException(Protocol.AccessTokenRequestErrorCodes.InvalidGrant); + } + + applied = true; + } + + return applied ? (MessageProtections?)MessageProtections.None : null; + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs new file mode 100644 index 0000000..09d35ee --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/OAuth2AuthorizationServerChannel.cs @@ -0,0 +1,124 @@ +//----------------------------------------------------------------------- +// <copyright file="OAuth2AuthorizationServerChannel.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. 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; + using DotNetOpenAuth.OAuth2.AuthServer.Messages; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// The channel for the OAuth protocol. + /// </summary> + internal class OAuth2AuthorizationServerChannel : OAuth2ChannelBase, IOAuth2ChannelWithAuthorizationServer { + /// <summary> + /// The messages receivable by this channel. + /// </summary> + private static readonly Type[] MessageTypes = new Type[] { + typeof(AccessTokenRefreshRequestAS), + typeof(AccessTokenAuthorizationCodeRequestAS), + typeof(AccessTokenResourceOwnerPasswordCredentialsRequest), + typeof(AccessTokenClientCredentialsRequest), + typeof(EndUserAuthorizationRequest), + typeof(EndUserAuthorizationImplicitRequest), + typeof(EndUserAuthorizationFailedResponse), + }; + + /// <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(MessageTypes, 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(); + ApplyMessageTemplate(response, webResponse); + 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(HttpRequestBase 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>(); + + // The order they are provided is used for outgoing messgaes, and reversed for incoming messages. + bindingElements.Add(new MessageValidationBindingElement()); + bindingElements.Add(new AccessTokenBindingElement()); + bindingElements.Add(new TokenCodeSerializationBindingElement()); + + return bindingElements.ToArray(); + } + } +} diff --git a/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/RefreshToken.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/RefreshToken.cs new file mode 100644 index 0000000..993583c --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/RefreshToken.cs @@ -0,0 +1,56 @@ +//----------------------------------------------------------------------- +// <copyright file="RefreshToken.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. 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.AuthorizationServer/OAuth2/ChannelElements/TokenCodeSerializationBindingElement.cs b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/TokenCodeSerializationBindingElement.cs new file mode 100644 index 0000000..b14f366 --- /dev/null +++ b/src/DotNetOpenAuth.OAuth2.AuthorizationServer/OAuth2/ChannelElements/TokenCodeSerializationBindingElement.cs @@ -0,0 +1,120 @@ +//----------------------------------------------------------------------- +// <copyright file="TokenCodeSerializationBindingElement.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth2.ChannelElements { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OAuth2.AuthServer.ChannelElements; + using DotNetOpenAuth.OAuth2.Messages; + + /// <summary> + /// Serializes and deserializes authorization codes, refresh tokens and access tokens + /// on incoming and outgoing messages. + /// </summary> + internal class TokenCodeSerializationBindingElement : AuthServerBindingElementBase { + /// <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 directResponse = message as IDirectResponseProtocolMessage; + var request = directResponse != null ? directResponse.OriginatingRequest as IAccessTokenRequestInternal : null; + + // Serialize the authorization code, if there is one. + var authCodeCarrier = message as IAuthorizationCodeCarryingRequest; + if (authCodeCarrier != null) { + var codeFormatter = AuthorizationCode.CreateFormatter(this.AuthorizationServer); + var code = authCodeCarrier.AuthorizationDescription; + authCodeCarrier.Code = codeFormatter.Serialize(code); + return MessageProtections.None; + } + + // Serialize the refresh token, if applicable. + var refreshTokenResponse = message as AccessTokenSuccessResponse; + if (refreshTokenResponse != null && refreshTokenResponse.HasRefreshToken) { + var refreshTokenCarrier = (IAuthorizationCarryingRequest)message; + var refreshToken = new RefreshToken(refreshTokenCarrier.AuthorizationDescription); + var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.CryptoKeyStore); + refreshTokenResponse.RefreshToken = refreshTokenFormatter.Serialize(refreshToken); + } + + // Serialize the access token, if applicable. + var accessTokenResponse = message as IAccessTokenIssuingResponse; + if (accessTokenResponse != null && accessTokenResponse.AuthorizationDescription != null) { + ErrorUtilities.VerifyInternal(request != null, "We should always have a direct request message for this case."); + var accessTokenFormatter = AccessToken.CreateFormatter(this.AuthorizationServer.AccessTokenSigningKey, request.AccessTokenCreationParameters.ResourceServerEncryptionKey); + accessTokenResponse.AccessToken = accessTokenFormatter.Serialize(accessTokenResponse.AuthorizationDescription); + } + + 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> + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "unauthorizedclient", Justification = "Protocol requirement")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "incorrectclientcredentials", Justification = "Protocol requirement")] + [SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "authorizationexpired", Justification = "Protocol requirement")] + [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "DotNetOpenAuth.Messaging.ErrorUtilities.VerifyProtocol(System.Boolean,System.String,System.Object[])", Justification = "Protocol requirement")] + public override MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { + var authCodeCarrier = message as IAuthorizationCodeCarryingRequest; + if (authCodeCarrier != null) { + var authorizationCodeFormatter = AuthorizationCode.CreateFormatter(this.AuthorizationServer); + var authorizationCode = authorizationCodeFormatter.Deserialize(message, authCodeCarrier.Code, Protocol.code); + authCodeCarrier.AuthorizationDescription = authorizationCode; + } + + var refreshTokenCarrier = message as IRefreshTokenCarryingRequest; + if (refreshTokenCarrier != null) { + var refreshTokenFormatter = RefreshToken.CreateFormatter(this.AuthorizationServer.CryptoKeyStore); + var refreshToken = refreshTokenFormatter.Deserialize(message, refreshTokenCarrier.RefreshToken, Protocol.refresh_token); + refreshTokenCarrier.AuthorizationDescription = refreshToken; + } + + return null; + } + } +} |