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.
///
SetupRequired,
///
/// Authentication is completed successfully.
///
Authenticated,
}
[DebuggerDisplay("Status: {Status}, ClaimedIdentifier: {ClaimedIdentifier}")]
class AuthenticationResponse : IAuthenticationResponse {
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);
}
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);
}
///
/// 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 {
[DebuggerStepThrough]
get { 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)); }
}
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,
IRelyingPartyApplicationStore store, Uri requestUrl) {
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) {
tokenEndpoint = Token.Deserialize(token, 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, store, requestUrl);
} else {
throw new OpenIdException(string.Format(CultureInfo.CurrentCulture,
Strings.InvalidOpenIdQueryParameterValue,
protocol.openid.mode, mode), query);
}
}
static AuthenticationResponse parseIdResResponse(IDictionary query,
ServiceEndpoint tokenEndpoint, ServiceEndpoint responseEndpoint,
IRelyingPartyApplicationStore store, Uri requestUrl) {
// 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(query, tokenEndpoint, responseEndpoint);
verifyNonceUnused(query, unverifiedEndpoint, store);
verifySignature(query, unverifiedEndpoint, store);
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));
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));
}
}
///
/// This is documented in OpenId Authentication 2.0 section 11.2.
///
static void verifyDiscoveredInfoMatchesAssertedInfo(IDictionary query,
ServiceEndpoint tokenEndpoint, ServiceEndpoint responseEndpoint) {
Logger.Debug("Verifying assertion matches identifier discovery results...");
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 !=
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 if the user
// gave us an OP Identifier originally, we need to perform discovery on
// the responseEndpoint.ClaimedIdentifier to verify the OP has authority
// to speak for it.
if (tokenEndpoint == null ||
tokenEndpoint.ClaimedIdentifier == tokenEndpoint.Protocol.ClaimedIdentifierForOPIdentifier) {
Identifier claimedIdentifier = Util.GetRequiredArg(query, responseEndpoint.Protocol.openid.claimed_id);
List discoveredEndpoints = new List(claimedIdentifier.Discover());
// Make sure the response endpoint matches one of the discovered endpoints.
if (!discoveredEndpoints.Contains(responseEndpoint)) {
throw new OpenIdException(Strings.IssuedAssertionFailsIdentifierDiscovery);
}
} else {
// Check that the assertion matches the service endpoint we know about.
if (responseEndpoint != tokenEndpoint)
throw new OpenIdException(Strings.IssuedAssertionFailsIdentifierDiscovery);
}
}
}
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(IDictionary query, ServiceEndpoint endpoint, IRelyingPartyApplicationStore store) {
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 = store != null ? 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(query, endpoint, store);
} 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(IDictionary query, ServiceEndpoint provider, IRelyingPartyApplicationStore store) {
var request = CheckAuthRequest.Create(provider, query);
if (request.Response.InvalidatedAssociationHandle != null && store != null)
store.RemoveAssociation(provider.ProviderEndpoint, request.Response.InvalidatedAssociationHandle);
if (!request.Response.IsAuthenticationValid)
throw new OpenIdException(Strings.InvalidSignature);
}
}
}