diff options
Diffstat (limited to 'src/DotNetOpenAuth.OpenIdOAuth/OAuth')
4 files changed, 321 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.OpenIdOAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs b/src/DotNetOpenAuth.OpenIdOAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs new file mode 100644 index 0000000..d6a7e93 --- /dev/null +++ b/src/DotNetOpenAuth.OpenIdOAuth/OAuth/ChannelElements/ICombinedOpenIdProviderTokenManager.cs @@ -0,0 +1,33 @@ +//----------------------------------------------------------------------- +// <copyright file="ICombinedOpenIdProviderTokenManager.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using DotNetOpenAuth.OpenId; + + /// <summary> + /// An interface that providers that play a dual role as OpenID Provider + /// and OAuth Service Provider should implement on their token manager classes. + /// </summary> + /// <remarks> + /// This interface should be implemented by the same class that implements + /// <see cref="ITokenManager"/> in order to enable the OpenID+OAuth extension. + /// </remarks> + public interface ICombinedOpenIdProviderTokenManager : IOpenIdOAuthTokenManager, ITokenManager { + /// <summary> + /// Gets the OAuth consumer key for a given OpenID relying party realm. + /// </summary> + /// <param name="realm">The relying party's OpenID realm.</param> + /// <returns>The OAuth consumer key for a given OpenID realm.</returns> + /// <para>This is a security-critical function. Since OpenID requests + /// and OAuth extensions for those requests can be formulated by ANYONE + /// (no signing is required by the relying party), and since the response to + /// the authentication will include access the user is granted to the + /// relying party who CLAIMS to be from some realm, it is of paramount + /// importance that the realm is recognized as belonging to the consumer + /// key by the host service provider in order to protect against phishers.</para> + string GetConsumerKey(Realm realm); + } +} diff --git a/src/DotNetOpenAuth.OpenIdOAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs b/src/DotNetOpenAuth.OpenIdOAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs new file mode 100644 index 0000000..3f3c1d9 --- /dev/null +++ b/src/DotNetOpenAuth.OpenIdOAuth/OAuth/ChannelElements/IOpenIdOAuthTokenManager.cs @@ -0,0 +1,30 @@ +//----------------------------------------------------------------------- +// <copyright file="IOpenIdOAuthTokenManager.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth.ChannelElements { + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + + /// <summary> + /// Additional methods an <see cref="ITokenManager"/> implementing class + /// may implement to support the OpenID+OAuth extension. + /// </summary> + public interface IOpenIdOAuthTokenManager { + /// <summary> + /// Stores a new request token obtained over an OpenID request. + /// </summary> + /// <param name="consumerKey">The consumer key.</param> + /// <param name="authorization">The authorization message carrying the request token and authorized access scope.</param> + /// <remarks> + /// <para>The token secret is the empty string.</para> + /// <para>Tokens stored by this method should be short-lived to mitigate + /// possible security threats. Their lifetime should be sufficient for the + /// relying party to receive the positive authentication assertion and immediately + /// send a follow-up request for the access token.</para> + /// </remarks> + void StoreOpenIdAuthorizedRequestToken(string consumerKey, AuthorizationApprovedResponse authorization); + } +} diff --git a/src/DotNetOpenAuth.OpenIdOAuth/OAuth/ServiceProviderOpenIdProvider.cs b/src/DotNetOpenAuth.OpenIdOAuth/OAuth/ServiceProviderOpenIdProvider.cs new file mode 100644 index 0000000..b590a90 --- /dev/null +++ b/src/DotNetOpenAuth.OpenIdOAuth/OAuth/ServiceProviderOpenIdProvider.cs @@ -0,0 +1,161 @@ +//----------------------------------------------------------------------- +// <copyright file="ServiceProviderOpenIdProvider.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; + using System.Globalization; + using System.Security.Principal; + using System.ServiceModel.Channels; + using System.Web; + using DotNetOpenAuth.Configuration; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Messaging.Bindings; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + using DotNetOpenAuth.OpenId; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + using DotNetOpenAuth.OpenId.Messages; + using DotNetOpenAuth.OpenId.Provider; + + /// <summary> + /// A web application that allows access via OAuth and can respond to OpenID+OAuth requests. + /// </summary> + /// <remarks> + /// <para>The Service Provider’s documentation should include:</para> + /// <list> + /// <item>The URLs (Request URLs) the Consumer will use when making OAuth requests, and the HTTP methods (i.e. GET, POST, etc.) used in the Request Token URL and Access Token URL.</item> + /// <item>Signature methods supported by the Service Provider.</item> + /// <item>Any additional request parameters that the Service Provider requires in order to obtain a Token. Service Provider specific parameters MUST NOT begin with oauth_.</item> + /// </list> + /// </remarks> + public class ServiceProviderOpenIdProvider : ServiceProvider { + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProviderOpenIdProvider"/> class. + /// </summary> + /// <param name="serviceDescription">The endpoints and behavior on the Service Provider.</param> + /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> + public ServiceProviderOpenIdProvider(ServiceProviderDescription serviceDescription, IServiceProviderTokenManager tokenManager) + : base(serviceDescription, tokenManager) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProviderOpenIdProvider"/> class. + /// </summary> + /// <param name="serviceDescription">The service description.</param> + /// <param name="tokenManager">The token manager.</param> + /// <param name="messageTypeProvider">The message type provider.</param> + public ServiceProviderOpenIdProvider(ServiceProviderDescription serviceDescription, IServiceProviderTokenManager tokenManager, OAuthServiceProviderMessageFactory messageTypeProvider) + : base(serviceDescription, tokenManager, messageTypeProvider) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProviderOpenIdProvider"/> class. + /// </summary> + /// <param name="serviceDescription">The service description.</param> + /// <param name="tokenManager">The token manager.</param> + /// <param name="nonceStore">The nonce store.</param> + public ServiceProviderOpenIdProvider(ServiceProviderDescription serviceDescription, IServiceProviderTokenManager tokenManager, INonceStore nonceStore) + : base(serviceDescription, tokenManager, nonceStore) { + } + + /// <summary> + /// Initializes a new instance of the <see cref="ServiceProviderOpenIdProvider"/> class. + /// </summary> + /// <param name="serviceDescription">The service description.</param> + /// <param name="tokenManager">The token manager.</param> + /// <param name="nonceStore">The nonce store.</param> + /// <param name="messageTypeProvider">The message type provider.</param> + public ServiceProviderOpenIdProvider(ServiceProviderDescription serviceDescription, IServiceProviderTokenManager tokenManager, INonceStore nonceStore, OAuthServiceProviderMessageFactory messageTypeProvider) + : base(serviceDescription, tokenManager, nonceStore, messageTypeProvider) { + } + + /// <summary> + /// Gets the OAuth authorization request included with an OpenID authentication + /// request, if there is one. + /// </summary> + /// <param name="openIdRequest">The OpenID authentication request.</param> + /// <returns> + /// The scope of access the relying party is requesting, or null if no OAuth request + /// is present. + /// </returns> + /// <remarks> + /// <para>Call this method rather than simply extracting the OAuth extension + /// out from the authentication request directly to ensure that the additional + /// security measures that are required are taken.</para> + /// </remarks> + public AuthorizationRequest ReadAuthorizationRequest(IHostProcessedRequest openIdRequest) { + Requires.NotNull(openIdRequest, "openIdRequest"); + Requires.ValidState(this.TokenManager is ICombinedOpenIdProviderTokenManager); + var openidTokenManager = this.TokenManager as ICombinedOpenIdProviderTokenManager; + ErrorUtilities.VerifyOperation(openidTokenManager != null, OAuthStrings.OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface, typeof(IOpenIdOAuthTokenManager).FullName); + + var authzRequest = openIdRequest.GetExtension<AuthorizationRequest>(); + if (authzRequest == null) { + return null; + } + + // OpenID+OAuth spec section 9: + // The Combined Provider SHOULD verify that the consumer key passed in the + // request is authorized to be used for the realm passed in the request. + string expectedConsumerKey = openidTokenManager.GetConsumerKey(openIdRequest.Realm); + ErrorUtilities.VerifyProtocol( + string.Equals(expectedConsumerKey, authzRequest.Consumer, StringComparison.Ordinal), + OAuthStrings.OpenIdOAuthRealmConsumerKeyDoNotMatch); + + return authzRequest; + } + + /// <summary> + /// Attaches the authorization response to an OpenID authentication response. + /// </summary> + /// <param name="openIdAuthenticationRequest">The OpenID authentication request.</param> + /// <param name="consumerKey">The consumer key. Must be <c>null</c> if and only if <paramref name="scope"/> is null.</param> + /// <param name="scope">The approved access scope. Use <c>null</c> to indicate no access was granted. The empty string will be interpreted as some default level of access is granted.</param> + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "We want to take IAuthenticationRequest because that's the only supported use case.")] + [Obsolete("Call the overload that doesn't take a consumerKey instead.")] + public void AttachAuthorizationResponse(IHostProcessedRequest openIdAuthenticationRequest, string consumerKey, string scope) { + Requires.NotNull(openIdAuthenticationRequest, "openIdAuthenticationRequest"); + Requires.True((consumerKey == null) == (scope == null), null); + Requires.ValidState(this.TokenManager is ICombinedOpenIdProviderTokenManager); + var openidTokenManager = (ICombinedOpenIdProviderTokenManager)this.TokenManager; + ErrorUtilities.VerifyArgument(consumerKey == null || consumerKey == openidTokenManager.GetConsumerKey(openIdAuthenticationRequest.Realm), OAuthStrings.OpenIdOAuthRealmConsumerKeyDoNotMatch); + + this.AttachAuthorizationResponse(openIdAuthenticationRequest, scope); + } + + /// <summary> + /// Attaches the authorization response to an OpenID authentication response. + /// </summary> + /// <param name="openIdAuthenticationRequest">The OpenID authentication request.</param> + /// <param name="scope">The approved access scope. Use <c>null</c> to indicate no access was granted. The empty string will be interpreted as some default level of access is granted.</param> + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "We want to take IAuthenticationRequest because that's the only supported use case.")] + public void AttachAuthorizationResponse(IHostProcessedRequest openIdAuthenticationRequest, string scope) { + Requires.NotNull(openIdAuthenticationRequest, "openIdAuthenticationRequest"); + Requires.ValidState(this.TokenManager is ICombinedOpenIdProviderTokenManager); + + var openidTokenManager = this.TokenManager as ICombinedOpenIdProviderTokenManager; + IOpenIdMessageExtension response; + if (scope != null) { + // Generate an authorized request token to return to the relying party. + string consumerKey = openidTokenManager.GetConsumerKey(openIdAuthenticationRequest.Realm); + var approvedResponse = new AuthorizationApprovedResponse { + RequestToken = this.TokenGenerator.GenerateRequestToken(consumerKey), + Scope = scope, + }; + openidTokenManager.StoreOpenIdAuthorizedRequestToken(consumerKey, approvedResponse); + response = approvedResponse; + } else { + response = new AuthorizationDeclinedResponse(); + } + + openIdAuthenticationRequest.AddResponseExtension(response); + } + } +} diff --git a/src/DotNetOpenAuth.OpenIdOAuth/OAuth/WebConsumerOpenIdRelyingParty.cs b/src/DotNetOpenAuth.OpenIdOAuth/OAuth/WebConsumerOpenIdRelyingParty.cs new file mode 100644 index 0000000..0d1a602 --- /dev/null +++ b/src/DotNetOpenAuth.OpenIdOAuth/OAuth/WebConsumerOpenIdRelyingParty.cs @@ -0,0 +1,97 @@ +//----------------------------------------------------------------------- +// <copyright file="WebConsumerOpenIdRelyingParty.cs" company="Outercurve Foundation"> +// Copyright (c) Outercurve Foundation. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.OAuth { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.OAuth.ChannelElements; + using DotNetOpenAuth.OAuth.Messages; + using DotNetOpenAuth.OpenId.Extensions.OAuth; + using DotNetOpenAuth.OpenId.RelyingParty; + + /// <summary> + /// A website or application that uses OAuth to access the Service Provider on behalf of the User + /// and can attach OAuth requests to outbound OpenID authentication requests. + /// </summary> + /// <remarks> + /// The methods on this class are thread-safe. Provided the properties are set and not changed + /// afterward, a single instance of this class may be used by an entire web application safely. + /// </remarks> + public class WebConsumerOpenIdRelyingParty : WebConsumer { + /// <summary> + /// Initializes a new instance of the <see cref="WebConsumerOpenIdRelyingParty"/> class. + /// </summary> + /// <param name="serviceDescription">The endpoints and behavior of the Service Provider.</param> + /// <param name="tokenManager">The host's method of storing and recalling tokens and secrets.</param> + public WebConsumerOpenIdRelyingParty(ServiceProviderDescription serviceDescription, IConsumerTokenManager tokenManager) + : base(serviceDescription, tokenManager) { + } + + /// <summary> + /// Attaches an OAuth authorization request to an outgoing OpenID authentication request. + /// </summary> + /// <param name="openIdAuthenticationRequest">The OpenID authentication request.</param> + /// <param name="scope">The scope of access that is requested of the service provider.</param> + public void AttachAuthorizationRequest(IAuthenticationRequest openIdAuthenticationRequest, string scope) { + Requires.NotNull(openIdAuthenticationRequest, "openIdAuthenticationRequest"); + + var authorizationRequest = new AuthorizationRequest { + Consumer = this.ConsumerKey, + Scope = scope, + }; + + openIdAuthenticationRequest.AddExtension(authorizationRequest); + } + + /// <summary> + /// Processes an incoming authorization-granted message from an SP and obtains an access token. + /// </summary> + /// <param name="openIdAuthenticationResponse">The OpenID authentication response that may be carrying an authorized request token.</param> + /// <returns> + /// The access token, or null if OAuth authorization was denied by the user or service provider. + /// </returns> + /// <remarks> + /// The access token, if granted, is automatically stored in the <see cref="ConsumerBase.TokenManager"/>. + /// The token manager instance must implement <see cref="IOpenIdOAuthTokenManager"/>. + /// </remarks> + public AuthorizedTokenResponse ProcessUserAuthorization(IAuthenticationResponse openIdAuthenticationResponse) { + Requires.NotNull(openIdAuthenticationResponse, "openIdAuthenticationResponse"); + Requires.ValidState(this.TokenManager is IOpenIdOAuthTokenManager); + var openidTokenManager = this.TokenManager as IOpenIdOAuthTokenManager; + ErrorUtilities.VerifyOperation(openidTokenManager != null, OAuthStrings.OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface, typeof(IOpenIdOAuthTokenManager).FullName); + + // The OAuth extension is only expected in positive assertion responses. + if (openIdAuthenticationResponse.Status != AuthenticationStatus.Authenticated) { + return null; + } + + // Retrieve the OAuth extension + var positiveAuthorization = openIdAuthenticationResponse.GetExtension<AuthorizationApprovedResponse>(); + if (positiveAuthorization == null) { + return null; + } + + // Prepare a message to exchange the request token for an access token. + // We are careful to use a v1.0 message version so that the oauth_verifier is not required. + var requestAccess = new AuthorizedTokenRequest(this.ServiceProvider.AccessTokenEndpoint, Protocol.V10.Version) { + RequestToken = positiveAuthorization.RequestToken, + ConsumerKey = this.ConsumerKey, + }; + + // Retrieve the access token and store it in the token manager. + openidTokenManager.StoreOpenIdAuthorizedRequestToken(this.ConsumerKey, positiveAuthorization); + var grantAccess = this.Channel.Request<AuthorizedTokenResponse>(requestAccess); + this.TokenManager.ExpireRequestTokenAndStoreNewAccessToken(this.ConsumerKey, positiveAuthorization.RequestToken, grantAccess.AccessToken, grantAccess.TokenSecret); + + // Provide the caller with the access token so it may be associated with the user + // that is logging in. + return grantAccess; + } + } +} |