namespace DotNetOpenId.RelyingParty {
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;
using System.Web;
using System.Globalization;
using System.Diagnostics;
using DotNetOpenId.Extensions;
///
/// An enumeration of the possible results of an authentication attempt.
///
public enum AuthenticationStatus {
///
/// The authentication was canceled by the user agent while at the provider.
///
Canceled,
///
/// The authentication failed because an error was detected in the OpenId communication.
///
Failed,
///
/// The Provider responded to a request for immediate authentication approval
/// with a message stating that additional user agent interaction is required
/// before authentication can be completed.
/// Casting the to a
/// in this case can help
/// you retry the authentication using setup (non-immediate) mode.
///
SetupRequired,
///
/// Authentication is completed successfully.
///
Authenticated,
}
[DebuggerDisplay("Status: {Status}, ClaimedIdentifier: {ClaimedIdentifier}")]
class AuthenticationResponse : IAuthenticationResponse, ISetupRequiredAuthenticationResponse {
internal AuthenticationResponse(AuthenticationStatus status, ServiceEndpoint provider, IDictionary query) {
if (provider == null) throw new ArgumentNullException("provider");
if (query == null) throw new ArgumentNullException("query");
if (status == AuthenticationStatus.Authenticated) {
Logger.InfoFormat("Verified positive authentication assertion for: {0}", provider.ClaimedIdentifier);
} else {
Logger.InfoFormat("Negative authentication assertion received: {0}", status);
}
// TODO: verify signature on callback args
CallbackArguments = cleanQueryForCallbackArguments(query);
Status = status;
Provider = provider;
signedArguments = new Dictionary();
string signed;
if (query.TryGetValue(Provider.Protocol.openid.signed, out signed)) {
foreach (string fieldNoPrefix in signed.Split(',')) {
string fieldWithPrefix = Provider.Protocol.openid.Prefix + fieldNoPrefix;
string val;
if (!query.TryGetValue(fieldWithPrefix, out val)) val = string.Empty;
signedArguments[fieldWithPrefix] = val;
}
}
// Only read extensions from signed argument list.
IncomingExtensions = ExtensionArgumentsManager.CreateIncomingExtensions(signedArguments);
}
internal IDictionary CallbackArguments;
public IDictionary GetCallbackArguments() {
// Return a copy so that the caller cannot change the contents.
return new Dictionary(CallbackArguments);
}
///
/// Gets a callback argument's value that was previously added using
/// .
///
/// The value of the argument, or null if the named parameter could not be found.
public string GetCallbackArgument(string key) {
if (String.IsNullOrEmpty(key)) throw new ArgumentNullException("key");
string value;
if (CallbackArguments.TryGetValue(key, out value)) {
return value;
}
return null;
}
///
/// The detailed success or failure status of the authentication attempt.
///
public AuthenticationStatus Status { get; private set; }
///
/// Details regarding a failed authentication attempt, if available.
/// This will only be set if is ,
/// but may sometimes by null in this case as well.
///
public Exception Exception { get { return null; } }
///
/// An Identifier that the end user claims to own.
///
public Identifier ClaimedIdentifier {
get {
if (Provider.ClaimedIdentifier == Provider.Protocol.ClaimedIdentifierForOPIdentifier) {
return null; // no claimed identifier -- failed directed identity authentication
}
return Provider.ClaimedIdentifier;
}
}
///
/// Gets a user-friendly OpenID Identifier for display purposes ONLY.
///
///
/// See .
///
public string FriendlyIdentifierForDisplay {
[DebuggerStepThrough]
get { return Provider.FriendlyIdentifierForDisplay; }
}
///
/// The discovered endpoint information.
///
internal ServiceEndpoint Provider { get; private set; }
///
/// The arguments returned from the OP that were signed.
///
IDictionary signedArguments;
///
/// Gets the set of arguments that the Provider included as extensions.
///
public ExtensionArgumentsManager IncomingExtensions { get; private set; }
internal Uri ReturnTo {
get { return new Uri(Util.GetRequiredArg(signedArguments, Provider.Protocol.openid.return_to)); }
}
internal string GetExtensionClientScript(Type extensionType) {
if (extensionType == null) throw new ArgumentNullException("extensionType");
if (!typeof(DotNetOpenId.Extensions.IClientScriptExtensionResponse).IsAssignableFrom(extensionType))
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
Strings.TypeMustImplementX, typeof(IClientScriptExtensionResponse).FullName),
"extensionType");
var extension = (IClientScriptExtensionResponse)Activator.CreateInstance(extensionType);
return GetExtensionClientScript(extension);
}
internal string GetExtensionClientScript(IClientScriptExtensionResponse extension) {
var fields = IncomingExtensions.GetExtensionArguments(extension.TypeUri);
if (fields != null) {
// The extension was found using the preferred TypeUri.
return extension.InitializeJavaScriptData(fields, this, extension.TypeUri);
} else {
// The extension may still be found using secondary TypeUris.
if (extension.AdditionalSupportedTypeUris != null) {
foreach (string typeUri in extension.AdditionalSupportedTypeUris) {
fields = IncomingExtensions.GetExtensionArguments(typeUri);
if (fields != null) {
// We found one of the older ones.
return extension.InitializeJavaScriptData(fields, this, typeUri);
}
}
}
}
return null;
}
bool getExtension(IExtensionResponse extension) {
var fields = IncomingExtensions.GetExtensionArguments(extension.TypeUri);
if (fields != null) {
// The extension was found using the preferred TypeUri.
return extension.Deserialize(fields, this, extension.TypeUri);
} else {
// The extension may still be found using secondary TypeUris.
if (extension.AdditionalSupportedTypeUris != null) {
foreach (string typeUri in extension.AdditionalSupportedTypeUris) {
fields = IncomingExtensions.GetExtensionArguments(typeUri);
if (fields != null) {
// We found one of the older ones.
return extension.Deserialize(fields, this, typeUri);
}
}
}
}
return false;
}
///
/// Tries to get an OpenID extension that may be present in the response.
///
/// The extension to retrieve.
/// The extension, if it is found. Null otherwise.
public T GetExtension() where T : IExtensionResponse, new() {
T extension = new T();
return getExtension(extension) ? extension : default(T);
}
public IExtensionResponse GetExtension(Type extensionType) {
if (extensionType == null) throw new ArgumentNullException("extensionType");
if (!typeof(DotNetOpenId.Extensions.IExtensionResponse).IsAssignableFrom(extensionType))
throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
Strings.TypeMustImplementX, typeof(IExtensionResponse).FullName),
"extensionType");
var extension = (IExtensionResponse)Activator.CreateInstance(extensionType);
return getExtension(extension) ? extension : null;
}
internal static AuthenticationResponse Parse(IDictionary query,
OpenIdRelyingParty relyingParty, Uri requestUrl, bool verifySignature) {
if (query == null) throw new ArgumentNullException("query");
if (requestUrl == null) throw new ArgumentNullException("requestUrl");
Logger.DebugFormat("OpenID authentication response received:{0}{1}", Environment.NewLine, Util.ToString(query));
ServiceEndpoint tokenEndpoint = null;
// The query parameter may be the POST query or the GET query,
// but the token parameter will always be in the GET query because
// it was added to the return_to parameter list.
IDictionary requestUrlQuery = Util.NameValueCollectionToDictionary(
HttpUtility.ParseQueryString(requestUrl.Query));
string token = Util.GetOptionalArg(requestUrlQuery, Token.TokenKey);
if (token != null) {
token = FixDoublyUriDecodedToken(token);
tokenEndpoint = Token.Deserialize(token, relyingParty.Store).Endpoint;
}
Protocol protocol = Protocol.Detect(query);
string mode = Util.GetRequiredArg(query, protocol.openid.mode);
if (mode.Equals(protocol.Args.Mode.cancel, StringComparison.Ordinal)) {
return new AuthenticationResponse(AuthenticationStatus.Canceled, tokenEndpoint, query);
} else if (mode.Equals(protocol.Args.Mode.setup_needed, StringComparison.Ordinal)) {
return new AuthenticationResponse(AuthenticationStatus.SetupRequired, tokenEndpoint, query);
} else if (mode.Equals(protocol.Args.Mode.error, StringComparison.Ordinal)) {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
"The provider returned an error: {0}", query[protocol.openid.error]));
} else if (mode.Equals(protocol.Args.Mode.id_res, StringComparison.Ordinal)) {
// We allow unsolicited assertions (that won't have our own token on it)
// only for OpenID 2.0 providers.
ServiceEndpoint responseEndpoint = null;
if (protocol.Version.Major < 2) {
if (tokenEndpoint == null)
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.MissingInternalQueryParameter, Token.TokenKey));
} else {
// 2.0 OPs provide enough information to assemble the entire endpoint info,
// except perhaps for the original user supplied identifier, which if available
// allows us to display a friendly XRI.
Identifier friendlyIdentifier = tokenEndpoint != null ? tokenEndpoint.UserSuppliedIdentifier : null;
responseEndpoint = ServiceEndpoint.ParseFromAuthResponse(query, friendlyIdentifier);
// If this is a solicited assertion, we'll have a token with endpoint data too,
// which we can use to more quickly confirm the validity of the claimed
// endpoint info.
}
// At this point, we are guaranteed to have tokenEndpoint ?? responseEndpoint
// set to endpoint data (one or the other or both).
// tokenEndpoint is known good data, whereas responseEndpoint must still be
// verified.
// For the error-handling and cancellation cases, the info does not have to
// be verified, so we'll use whichever one is available.
return parseIdResResponse(query, tokenEndpoint, responseEndpoint, relyingParty, requestUrl, verifySignature);
} else {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.InvalidOpenIdQueryParameterValue,
protocol.openid.mode, mode), query);
}
}
///
/// Corrects any URI decoding the Provider may have inappropriately done
/// to our return_to URL, resulting in an otherwise corrupted base64 token.
///
/// The token, which MAY have been corrupted by an extra URI decode.
/// The token; corrected if corruption had occurred.
///
/// AOL may have incorrectly URI-decoded the token for us in the return_to,
/// resulting in a token URI-decoded twice by the time we see it, and no
/// longer being a valid base64 string.
/// It turns out that the only symbols from base64 that is also encoded
/// in URI encoding rules are the + and / characters.
/// AOL decodes the %2b sequence to the + character
/// and the %2f sequence to the / character (it shouldn't decode at all).
/// When we do our own URI decoding, the + character becomes a space (corrupting base64)
/// but the / character remains a /, so no further corruption happens to this character.
/// So to correct this we just need to change any spaces we find in the token
/// back to + characters.
///
private static string FixDoublyUriDecodedToken(string token) {
if (token == null) throw new ArgumentNullException("token");
if (token.Contains(" ")) {
Logger.Error("Deserializing a corrupted token. The OpenID Provider may have inappropriately decoded the return_to URL before sending it back to us.");
token = token.Replace(' ', '+'); // Undo any extra decoding the Provider did
}
return token;
}
static AuthenticationResponse parseIdResResponse(IDictionary query,
ServiceEndpoint tokenEndpoint, ServiceEndpoint responseEndpoint,
OpenIdRelyingParty relyingParty, Uri requestUrl, bool verifyMessageSignature) {
// Use responseEndpoint if it is available so we get the
// Claimed Identifer correct in the AuthenticationResponse.
ServiceEndpoint unverifiedEndpoint = responseEndpoint ?? tokenEndpoint;
if (unverifiedEndpoint.Protocol.Version.Major < 2) {
string user_setup_url = Util.GetOptionalArg(query, unverifiedEndpoint.Protocol.openid.user_setup_url);
if (user_setup_url != null) {
return new AuthenticationResponse(AuthenticationStatus.SetupRequired, unverifiedEndpoint, query);
}
}
verifyReturnTo(query, unverifiedEndpoint, requestUrl);
verifyDiscoveredInfoMatchesAssertedInfo(relyingParty, query, tokenEndpoint, responseEndpoint);
if (verifyMessageSignature) {
verifyNonceUnused(query, unverifiedEndpoint, relyingParty.Store);
verifySignature(relyingParty, query, unverifiedEndpoint);
}
return new AuthenticationResponse(AuthenticationStatus.Authenticated, unverifiedEndpoint, query);
}
///
/// Verifies that the openid.return_to field matches the URL of the actual HTTP request.
///
///
/// From OpenId Authentication 2.0 section 11.1:
/// To verify that the "openid.return_to" URL matches the URL that is processing this assertion:
/// * The URL scheme, authority, and path MUST be the same between the two URLs.
/// * Any query parameters that are present in the "openid.return_to" URL MUST
/// also be present with the same values in the URL of the HTTP request the RP received.
///
static void verifyReturnTo(IDictionary query, ServiceEndpoint endpoint, Uri requestUrl) {
Debug.Assert(query != null);
Debug.Assert(endpoint != null);
Debug.Assert(requestUrl != null);
Logger.Debug("Verifying return_to...");
Uri return_to = new Uri(Util.GetRequiredArg(query, endpoint.Protocol.openid.return_to));
if (return_to.Scheme != requestUrl.Scheme ||
return_to.Authority != requestUrl.Authority ||
return_to.AbsolutePath != requestUrl.AbsolutePath)
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.ReturnToParamDoesNotMatchRequestUrl, endpoint.Protocol.openid.return_to,
return_to, requestUrl));
NameValueCollection returnToArgs = HttpUtility.ParseQueryString(return_to.Query);
NameValueCollection requestArgs = HttpUtility.ParseQueryString(requestUrl.Query);
foreach (string paramName in returnToArgs) {
if (requestArgs[paramName] != returnToArgs[paramName])
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.ReturnToParamDoesNotMatchRequestUrl, endpoint.Protocol.openid.return_to,
return_to, requestUrl));
}
}
///
/// This is documented in OpenId Authentication 2.0 section 11.2.
///
static void verifyDiscoveredInfoMatchesAssertedInfo(OpenIdRelyingParty relyingParty,
IDictionary query,
ServiceEndpoint tokenEndpoint, ServiceEndpoint responseEndpoint) {
Logger.Debug("Verifying assertion matches identifier discovery results...");
// Verify that the actual version of the OP endpoint matches discovery.
Protocol actualProtocol = Protocol.Detect(query);
Protocol discoveredProtocol = (tokenEndpoint ?? responseEndpoint).Protocol;
if (!actualProtocol.Equals(discoveredProtocol)) {
// Allow an exception so that v1.1 and v1.0 can be seen as identical for this
// verification. v1.0 has no spec, and v1.1 and v1.0 cannot be clearly distinguished
// from the protocol, so detecting their differences is meaningless, and throwing here
// would just break thing unnecessarily.
if (!(actualProtocol.Version.Major == 1 && discoveredProtocol.Version.Major == 1)) {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.OpenIdDiscoveredAndActualVersionMismatch,
actualProtocol.Version, discoveredProtocol.Version));
}
}
if ((tokenEndpoint ?? responseEndpoint).Protocol.Version.Major < 2) {
Debug.Assert(tokenEndpoint != null, "Our OpenID 1.x implementation requires an RP token. And this should have been verified by our caller.");
// For 1.x OPs, we only need to verify that the OP Local Identifier
// hasn't changed since we made the request.
if (tokenEndpoint.ProviderLocalIdentifier !=
((Identifier)Util.GetRequiredArg(query, tokenEndpoint.Protocol.openid.identity))) {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.TamperingDetected, tokenEndpoint.Protocol.openid.identity,
tokenEndpoint.ProviderLocalIdentifier,
Util.GetRequiredArg(query, tokenEndpoint.Protocol.openid.identity)));
}
} else {
// In 2.0, we definitely have a responseEndpoint, but may not have a
// tokenEndpoint. If we don't have a tokenEndpoint, or it doesn't match the assertion,
// or if the user gave us an OP Identifier originally, then we need to perform discovery on
// the responseEndpoint.ClaimedIdentifier to verify the OP has authority
// to speak for it.
if (tokenEndpoint == null || // no token included (unsolicited assertion)
tokenEndpoint != responseEndpoint || // the OP is asserting something different than we asked for
tokenEndpoint.ClaimedIdentifier == ((Identifier)tokenEndpoint.Protocol.ClaimedIdentifierForOPIdentifier)) { // or directed identity is in effect
Identifier claimedIdentifier = Util.GetRequiredArg(query, responseEndpoint.Protocol.openid.claimed_id);
// Require SSL where appropriate. This will filter out insecure identifiers,
// redirects and provider endpoints automatically. If we find a match after all that
// filtering with the responseEndpoint, then the unsolicited assertion is secure.
if (relyingParty.Settings.RequireSsl && !claimedIdentifier.TryRequireSsl(out claimedIdentifier)) {
throw new OpenIdException(Strings.InsecureWebRequestWithSslRequired, query);
}
Logger.InfoFormat("Provider asserted an identifier that requires (re)discovery to confirm.");
List discoveredEndpoints = new List(claimedIdentifier.Discover());
// Make sure the response endpoint matches one of the discovered endpoints.
if (!discoveredEndpoints.Contains(responseEndpoint)) {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.IssuedAssertionFailsIdentifierDiscovery,
responseEndpoint, Util.ToString(discoveredEndpoints)));
}
}
}
}
static void verifyNonceUnused(IDictionary query, ServiceEndpoint endpoint, IRelyingPartyApplicationStore store) {
if (endpoint.Protocol.Version.Major < 2) return; // nothing to validate
if (store == null) return; // we'll pass verifying the nonce responsibility to the OP
Logger.Debug("Verifying nonce is unused...");
var nonce = new Nonce(Util.GetRequiredArg(query, endpoint.Protocol.openid.response_nonce), true);
nonce.Consume(store);
}
static void verifySignature(OpenIdRelyingParty relyingParty, IDictionary query, ServiceEndpoint endpoint) {
string signed = Util.GetRequiredArg(query, endpoint.Protocol.openid.signed);
string[] signedFields = signed.Split(',');
// Check that all fields that are required to be signed are indeed signed
if (endpoint.Protocol.Version.Major >= 2) {
verifyFieldsAreSigned(signedFields,
endpoint.Protocol.openidnp.op_endpoint,
endpoint.Protocol.openidnp.return_to,
endpoint.Protocol.openidnp.response_nonce,
endpoint.Protocol.openidnp.assoc_handle);
if (query.ContainsKey(endpoint.Protocol.openid.claimed_id))
verifyFieldsAreSigned(signedFields,
endpoint.Protocol.openidnp.claimed_id,
endpoint.Protocol.openidnp.identity);
} else {
verifyFieldsAreSigned(signedFields,
endpoint.Protocol.openidnp.identity,
endpoint.Protocol.openidnp.return_to);
}
// Now actually validate the signature itself.
string assoc_handle = Util.GetRequiredArg(query, endpoint.Protocol.openid.assoc_handle);
Association assoc = relyingParty.Store != null ? relyingParty.Store.GetAssociation(endpoint.ProviderEndpoint, assoc_handle) : null;
if (assoc == null) {
// It's not an association we know about. Dumb mode is our
// only possible path for recovery.
Logger.Debug("Passing signature back to Provider for verification (no association available)...");
verifySignatureByProvider(relyingParty, query, endpoint);
} else {
if (assoc.IsExpired)
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
"Association with {0} expired", endpoint.ProviderEndpoint), endpoint.ClaimedIdentifier);
Logger.Debug("Verifying signature by association...");
verifySignatureByAssociation(query, endpoint.Protocol, signedFields, assoc);
}
}
///
/// Checks that fields that must be signed are in fact signed.
///
static void verifyFieldsAreSigned(string[] fieldsThatAreSigned, params string[] fieldsThatShouldBeSigned) {
Debug.Assert(fieldsThatAreSigned != null);
Debug.Assert(fieldsThatShouldBeSigned != null);
foreach (string field in fieldsThatShouldBeSigned) {
if (Array.IndexOf(fieldsThatAreSigned, field) < 0)
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.FieldMustBeSigned, field));
}
}
///
/// Verifies that a query is signed and that the signed fields have not been tampered with.
///
/// Thrown when the signature is missing or the query has been tampered with.
static void verifySignatureByAssociation(IDictionary query, Protocol protocol, string[] signedFields, Association assoc) {
string sig = Util.GetRequiredArg(query, protocol.openid.sig);
string v_sig = Convert.ToBase64String(assoc.Sign(query, signedFields, protocol.openid.Prefix));
if (v_sig != sig)
throw new OpenIdException(Strings.InvalidSignature);
}
///
/// Performs a dumb-mode authentication verification by making an extra
/// request to the provider after the user agent was redirected back
/// to the consumer site with an authenticated status.
///
/// Whether the authentication is valid.
static void verifySignatureByProvider(OpenIdRelyingParty relyingParty, IDictionary query, ServiceEndpoint provider) {
var request = CheckAuthRequest.Create(relyingParty, provider, query);
if (request.Response.InvalidatedAssociationHandle != null && relyingParty.Store != null)
relyingParty.Store.RemoveAssociation(provider.ProviderEndpoint, request.Response.InvalidatedAssociationHandle);
if (!request.Response.IsAuthenticationValid)
throw new OpenIdException(Strings.InvalidSignature);
}
static IDictionary cleanQueryForCallbackArguments(IDictionary query) {
var dictionary = new Dictionary();
foreach (var pair in query) {
// Disallow lookup of any openid parameters.
if (pair.Key.StartsWith("openid.", StringComparison.OrdinalIgnoreCase)) {
continue;
}
dictionary.Add(pair.Key, pair.Value);
}
return dictionary;
}
#region ISetupRequiredAuthenticationResponse Members
///
/// The to pass to
/// in a subsequent authentication attempt.
///
///
/// When directed identity is used, this will be the Provider Identifier given by the user.
/// Otherwise it will be the Claimed Identifier derived from the user-supplied identifier.
///
public Identifier ClaimedOrProviderIdentifier {
get {
if (Status != AuthenticationStatus.SetupRequired) {
throw new InvalidOperationException(Strings.OperationOnlyValidForSetupRequiredState);
}
return ClaimedIdentifier ?? Provider.UserSuppliedIdentifier;
}
}
#endregion
}
}