//----------------------------------------------------------------------- // // Copyright (c) Outercurve Foundation. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOpenAuth.Test.OpenId { using System; using System.Net.Http; using System.Threading.Tasks; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.Messaging.Bindings; using DotNetOpenAuth.OpenId; using DotNetOpenAuth.OpenId.Messages; using DotNetOpenAuth.OpenId.Provider; using DotNetOpenAuth.OpenId.RelyingParty; using DotNetOpenAuth.Test.Mocks; using NUnit.Framework; using Validation; [TestFixture] public class AuthenticationTests : OpenIdTestBase { [SetUp] public override void SetUp() { base.SetUp(); } [Test] public async Task SharedAssociationPositive() { await this.ParameterizedAuthenticationTestAsync(true, true, false); } /// /// Verifies that a shared association protects against tampering. /// [Test] public async Task SharedAssociationTampered() { await this.ParameterizedAuthenticationTestAsync(true, true, true); } [Test] public async Task SharedAssociationNegative() { await this.ParameterizedAuthenticationTestAsync(true, false, false); } [Test] public async Task PrivateAssociationPositive() { await this.ParameterizedAuthenticationTestAsync(false, true, false); } /// /// Verifies that a private association protects against tampering. /// [Test] public async Task PrivateAssociationTampered() { await this.ParameterizedAuthenticationTestAsync(false, true, true); } [Test] public async Task NoAssociationNegative() { await this.ParameterizedAuthenticationTestAsync(false, false, false); } [Test] public async Task UnsolicitedAssertion() { var opStore = new StandardProviderApplicationStore(); var coordinator = new CoordinatorBase( async (hostFactories, ct) => { var op = new OpenIdProvider(opStore); Identifier id = GetMockIdentifier(ProtocolVersion.V20); var assertion = await op.PrepareUnsolicitedAssertionAsync(OPUri, RPRealmUri, id, OPLocalIdentifiers[0], ct); using (var httpClient = hostFactories.CreateHttpClient()) { using (var response = await httpClient.GetAsync(assertion.Headers.Location)) { response.EnsureSuccessStatusCode(); } } }, Handle(RPRealmUri).By(async (hostFactories, req, ct) => { var rp = new OpenIdRelyingParty(new StandardRelyingPartyApplicationStore(), hostFactories); IAuthenticationResponse response = await rp.GetResponseAsync(); Assert.AreEqual(AuthenticationStatus.Authenticated, response.Status); return new HttpResponseMessage(); }), Handle(OPUri).By( async (req, ct) => { var op = new OpenIdProvider(opStore); return await this.AutoProviderActionAsync(op, req, ct); }), MockHttpRequest.RegisterMockRPDiscovery(ssl: false)); await coordinator.RunAsync(); } [Test] public async Task UnsolicitedAssertionRejected() { var opStore = new StandardProviderApplicationStore(); var coordinator = new CoordinatorBase( async (hostFactories, ct) => { var op = new OpenIdProvider(opStore); Identifier id = GetMockIdentifier(ProtocolVersion.V20); var assertion = await op.PrepareUnsolicitedAssertionAsync(OPUri, RPRealmUri, id, OPLocalIdentifiers[0], ct); using (var httpClient = hostFactories.CreateHttpClient()) { using (var response = await httpClient.GetAsync(assertion.Headers.Location, ct)) { response.EnsureSuccessStatusCode(); } } }, Handle(RPRealmUri).By(async (hostFactories, req, ct) => { var rp = new OpenIdRelyingParty(new StandardRelyingPartyApplicationStore(), hostFactories); rp.SecuritySettings.RejectUnsolicitedAssertions = true; IAuthenticationResponse response = await rp.GetResponseAsync(req, ct); Assert.AreEqual(AuthenticationStatus.Failed, response.Status); return new HttpResponseMessage(); }), Handle(OPUri).By(async (hostFactories, req, ct) => { var op = new OpenIdProvider(opStore); return await this.AutoProviderActionAsync(op, req, ct); }), MockHttpRequest.RegisterMockRPDiscovery(false)); await coordinator.RunAsync(); } /// /// Verifies that delegating identifiers are rejected in unsolicited assertions /// when the appropriate security setting is set. /// [Test] public async Task UnsolicitedDelegatingIdentifierRejection() { var opStore = new StandardProviderApplicationStore(); var coordinator = new CoordinatorBase( async (hostFactories, ct) => { var op = new OpenIdProvider(opStore); Identifier id = GetMockIdentifier(ProtocolVersion.V20, false, true); var assertion = await op.PrepareUnsolicitedAssertionAsync(OPUri, RPRealmUri, id, OPLocalIdentifiers[0], ct); using (var httpClient = hostFactories.CreateHttpClient()) { using (var response = await httpClient.GetAsync(assertion.Headers.Location, ct)) { response.EnsureSuccessStatusCode(); } } }, Handle(RPRealmUri).By(async (hostFactories, req, ct) => { var rp = new OpenIdRelyingParty(new StandardRelyingPartyApplicationStore(), hostFactories); rp.SecuritySettings.RejectDelegatingIdentifiers = true; IAuthenticationResponse response = await rp.GetResponseAsync(req, ct); Assert.AreEqual(AuthenticationStatus.Failed, response.Status); return new HttpResponseMessage(); }), Handle(OPUri).By(async (hostFactories, req, ct) => { var op = new OpenIdProvider(opStore); return await this.AutoProviderActionAsync(op, req, ct); }), MockHttpRequest.RegisterMockRPDiscovery(false)); await coordinator.RunAsync(); } private async Task ParameterizedAuthenticationTestAsync(bool sharedAssociation, bool positive, bool tamper) { foreach (Protocol protocol in Protocol.AllPracticalVersions) { foreach (bool statelessRP in new[] { false, true }) { if (sharedAssociation && statelessRP) { // Skip the invalid combination scenario. continue; } foreach (bool immediate in new[] { false, true }) { TestLogger.InfoFormat("Beginning authentication test scenario. OpenID: {0}, Shared: {1}, positive: {2}, tamper: {3}, stateless: {4}, immediate: {5}", protocol.Version, sharedAssociation, positive, tamper, statelessRP, immediate); await this.ParameterizedAuthenticationTestAsync(protocol, statelessRP, sharedAssociation, positive, immediate, tamper); } } } } private async Task ParameterizedAuthenticationTestAsync(Protocol protocol, bool statelessRP, bool sharedAssociation, bool positive, bool immediate, bool tamper) { Requires.That(!statelessRP || !sharedAssociation, null, "The RP cannot be stateless while sharing an association with the OP."); Requires.That(positive || !tamper, null, "Cannot tamper with a negative response."); var securitySettings = new ProviderSecuritySettings(); var cryptoKeyStore = new MemoryCryptoKeyStore(); var associationStore = new ProviderAssociationHandleEncoder(cryptoKeyStore); Association association = sharedAssociation ? HmacShaAssociationProvider.Create(protocol, protocol.Args.SignatureAlgorithm.Best, AssociationRelyingPartyType.Smart, associationStore, securitySettings) : null; int opStep = 0; var coordinator = new CoordinatorBase( RelyingPartyDriver(async (rp, ct) => { if (statelessRP) { rp = new OpenIdRelyingParty(null, rp.Channel.HostFactories); } var request = new CheckIdRequest(protocol.Version, OPUri, immediate ? AuthenticationRequestMode.Immediate : AuthenticationRequestMode.Setup); if (association != null) { StoreAssociation(rp, OPUri, association); request.AssociationHandle = association.Handle; } request.ClaimedIdentifier = "http://claimedid"; request.LocalIdentifier = "http://localid"; request.ReturnTo = RPUri; request.Realm = RPUri; var redirectRequest = await rp.Channel.PrepareResponseAsync(request, ct); Uri redirectResponse; using (var httpClient = rp.Channel.HostFactories.CreateHttpClient()) { using (var response = await httpClient.GetAsync(redirectRequest.Headers.Location)) { redirectResponse = response.Headers.Location; } } var assertionMessage = new HttpRequestMessage(HttpMethod.Get, redirectResponse.AbsoluteUri); if (positive) { if (tamper) { try { await rp.Channel.ReadFromRequestAsync(assertionMessage, ct); Assert.Fail("Expected exception {0} not thrown.", typeof(InvalidSignatureException).Name); } catch (InvalidSignatureException) { TestLogger.InfoFormat("Caught expected {0} exception after tampering with signed data.", typeof(InvalidSignatureException).Name); } } else { var response = await rp.Channel.ReadFromRequestAsync(assertionMessage, ct); Assert.IsNotNull(response); Assert.AreEqual(request.ClaimedIdentifier, response.ClaimedIdentifier); Assert.AreEqual(request.LocalIdentifier, response.LocalIdentifier); Assert.AreEqual(request.ReturnTo, response.ReturnTo); // Attempt to replay the message and verify that it fails. // Because in various scenarios and protocol versions different components // notice the replay, we can get one of two exceptions thrown. // When the OP notices the replay we get a generic InvalidSignatureException. // When the RP notices the replay we get a specific ReplayMessageException. try { // TODO: fix this. ////CoordinatingChannel channel = (CoordinatingChannel)rp.Channel; ////await channel.ReplayAsync(response); Assert.Fail("Expected ProtocolException was not thrown."); } catch (ProtocolException ex) { Assert.IsTrue(ex is ReplayedMessageException || ex is InvalidSignatureException, "A {0} exception was thrown instead of the expected {1} or {2}.", ex.GetType(), typeof(ReplayedMessageException).Name, typeof(InvalidSignatureException).Name); } } } else { var response = await rp.Channel.ReadFromRequestAsync(assertionMessage, ct); Assert.IsNotNull(response); if (immediate) { // Only 1.1 was required to include user_setup_url if (protocol.Version.Major < 2) { Assert.IsNotNull(response.UserSetupUrl); } } else { Assert.IsNull(response.UserSetupUrl); } } }), HandleProvider(async (op, req, ct) => { if (association != null) { var key = cryptoKeyStore.GetCurrentKey(ProviderAssociationHandleEncoder.AssociationHandleEncodingSecretBucket, TimeSpan.FromSeconds(1)); op.CryptoKeyStore.StoreKey(ProviderAssociationHandleEncoder.AssociationHandleEncodingSecretBucket, key.Key, key.Value); } switch (++opStep) { case 1: var request = await op.Channel.ReadFromRequestAsync(req, ct); Assert.IsNotNull(request); IProtocolMessage response; if (positive) { response = new PositiveAssertionResponse(request); } else { response = new NegativeAssertionResponse(request.Version, request.ReturnTo, request.Mode); } return await op.Channel.PrepareResponseAsync(response, ct); case 2: if (positive && (statelessRP || !sharedAssociation)) { var checkauthRequest = await op.Channel.ReadFromRequestAsync(req, ct); var checkauthResponse = new CheckAuthenticationResponse(checkauthRequest.Version, checkauthRequest); checkauthResponse.IsValid = checkauthRequest.IsValid; return await op.Channel.PrepareResponseAsync(checkauthResponse, ct); } throw Assumes.NotReachable(); case 3: if (positive && (statelessRP || !sharedAssociation)) { if (!tamper) { // Respond to the replay attack. var checkauthRequest = await op.Channel.ReadFromRequestAsync(req, ct); var checkauthResponse = new CheckAuthenticationResponse(checkauthRequest.Version, checkauthRequest); checkauthResponse.IsValid = checkauthRequest.IsValid; return await op.Channel.PrepareResponseAsync(checkauthResponse, ct); } } throw Assumes.NotReachable(); default: throw Assumes.NotReachable(); } })); if (tamper) { // TODO: fix this. ////coordinator.IncomingMessageFilter = message => { //// var assertion = message as PositiveAssertionResponse; //// if (assertion != null) { //// // Alter the Local Identifier between the Provider and the Relying Party. //// // If the signature binding element does its job, this should cause the RP //// // to throw. //// assertion.LocalIdentifier = "http://victim"; //// } ////}; } await coordinator.RunAsync(); } } }