//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.Test.OpenId {
using System;
using System.Threading;
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;
[TestFixture]
public class AssociationHandshakeTests : OpenIdTestBase {
[SetUp]
public override void SetUp() {
base.SetUp();
}
[Test]
public async Task AssociateUnencrypted() {
await this.ParameterizedAssociationTestAsync(OPUriSsl);
}
[Test]
public async Task AssociateDiffieHellmanOverHttp() {
await this.ParameterizedAssociationTestAsync(OPUri);
}
///
/// Verifies that the Provider can do Diffie-Hellman over HTTPS.
///
///
/// Some OPs out there flatly refuse to do this, and the spec doesn't forbid
/// putting the two together, so we verify that DNOI can handle it.
///
[Test]
public async Task AssociateDiffieHellmanOverHttps() {
Protocol protocol = Protocol.V20;
this.RegisterAutoProvider();
var rp = this.CreateRelyingParty();
// We have to formulate the associate request manually,
// since the DNOI RP won't voluntarily use DH on HTTPS.
var request = new AssociateDiffieHellmanRequest(protocol.Version, OPUri) {
AssociationType = protocol.Args.SignatureAlgorithm.HMAC_SHA256,
SessionType = protocol.Args.SessionType.DH_SHA256
};
request.InitializeRequest();
var response = await rp.Channel.RequestAsync(request, CancellationToken.None);
Assert.IsNotNull(response);
Assert.AreEqual(request.AssociationType, response.AssociationType);
Assert.AreEqual(request.SessionType, response.SessionType);
}
///
/// Verifies that the RP and OP can renegotiate an association type if the RP's
/// initial request for an association is for a type the OP doesn't support.
///
[Test]
public async Task AssociateRenegotiateBitLength() {
Protocol protocol = Protocol.V20;
// The strategy is to make a simple request of the RP to establish an association,
// and to more carefully observe the Provider-side of things to make sure that both
// the OP and RP are behaving as expected.
int providerAttemptCount = 0;
HandleProvider(
async (op, request) => {
op.SecuritySettings.MaximumHashBitLength = 160; // Force OP to reject HMAC-SHA256
switch (++providerAttemptCount) {
case 1:
// Receive initial request for an HMAC-SHA256 association.
var req = (AutoResponsiveRequest)await op.GetRequestAsync(request);
var associateRequest = (AssociateRequest)req.RequestMessage;
Assert.That(associateRequest, Is.Not.Null);
Assert.AreEqual(protocol.Args.SignatureAlgorithm.HMAC_SHA256, associateRequest.AssociationType);
// Ensure that the response is a suggestion that the RP try again with HMAC-SHA1
var renegotiateResponse =
(AssociateUnsuccessfulResponse)await req.GetResponseMessageAsyncTestHook(CancellationToken.None);
Assert.AreEqual(protocol.Args.SignatureAlgorithm.HMAC_SHA1, renegotiateResponse.AssociationType);
return await op.PrepareResponseAsync(req);
case 2:
// Receive second attempt request for an HMAC-SHA1 association.
req = (AutoResponsiveRequest)await op.GetRequestAsync(request);
associateRequest = (AssociateRequest)req.RequestMessage;
Assert.AreEqual(protocol.Args.SignatureAlgorithm.HMAC_SHA1, associateRequest.AssociationType);
// Ensure that the response is a success response.
var successResponse =
(AssociateSuccessfulResponse)await req.GetResponseMessageAsyncTestHook(CancellationToken.None);
Assert.AreEqual(protocol.Args.SignatureAlgorithm.HMAC_SHA1, successResponse.AssociationType);
return await op.PrepareResponseAsync(req);
default:
throw Assumes.NotReachable();
}
});
var rp = this.CreateRelyingParty();
var opDescription = new ProviderEndpointDescription(OPUri, protocol.Version);
Association association = await rp.AssociationManager.GetOrCreateAssociationAsync(opDescription, CancellationToken.None);
Assert.IsNotNull(association, "Association failed to be created.");
Assert.AreEqual(protocol.Args.SignatureAlgorithm.HMAC_SHA1, association.GetAssociationType(protocol));
}
///
/// Verifies that the OP rejects an associate request that has no encryption (transport or DH).
///
///
/// Verifies OP's compliance with OpenID 2.0 section 8.4.1.
///
[Test]
public async Task OPRejectsHttpNoEncryptionAssociateRequests() {
Protocol protocol = Protocol.V20;
this.RegisterAutoProvider();
var rp = this.CreateRelyingParty();
// We have to formulate the associate request manually,
// since the DNOA RP won't voluntarily suggest no encryption at all.
var request = new AssociateUnencryptedRequestNoSslCheck(protocol.Version, OPUri);
request.AssociationType = protocol.Args.SignatureAlgorithm.HMAC_SHA256;
request.SessionType = protocol.Args.SessionType.NoEncryption;
var response = await rp.Channel.RequestAsync(request, CancellationToken.None);
Assert.IsNotNull(response);
}
///
/// Verifies that the OP rejects an associate request
/// when the HMAC and DH bit lengths do not match.
///
[Test]
public async Task OPRejectsMismatchingAssociationAndSessionTypes() {
Protocol protocol = Protocol.V20;
this.RegisterAutoProvider();
var rp = this.CreateRelyingParty();
// We have to formulate the associate request manually,
// since the DNOI RP won't voluntarily mismatch the association and session types.
var request = new AssociateDiffieHellmanRequest(protocol.Version, OPUri);
request.AssociationType = protocol.Args.SignatureAlgorithm.HMAC_SHA256;
request.SessionType = protocol.Args.SessionType.DH_SHA1;
request.InitializeRequest();
var response = await rp.Channel.RequestAsync(request, CancellationToken.None);
Assert.IsNotNull(response);
Assert.AreEqual(protocol.Args.SignatureAlgorithm.HMAC_SHA1, response.AssociationType);
Assert.AreEqual(protocol.Args.SessionType.DH_SHA1, response.SessionType);
}
///
/// Verifies that the RP quietly rejects an OP that suggests an unknown association type.
///
[Test]
public async Task RPRejectsUnrecognizedAssociationType() {
Protocol protocol = Protocol.V20;
HandleProvider(
async (op, req) => {
// Receive initial request.
var request = await op.Channel.ReadFromRequestAsync(req, CancellationToken.None);
// Send a response that suggests a foreign association type.
var renegotiateResponse = new AssociateUnsuccessfulResponse(request.Version, request);
renegotiateResponse.AssociationType = "HMAC-UNKNOWN";
renegotiateResponse.SessionType = "DH-UNKNOWN";
return await op.Channel.PrepareResponseAsync(renegotiateResponse);
});
var rp = this.CreateRelyingParty();
var association = await rp.AssociationManager.GetOrCreateAssociationAsync(new ProviderEndpointDescription(OPUri, protocol.Version), CancellationToken.None);
Assert.IsNull(association, "The RP should quietly give up when the OP misbehaves.");
}
///
/// Verifies that the RP quietly rejects an OP that suggests an no encryption over an HTTP channel.
///
///
/// Verifies RP's compliance with OpenID 2.0 section 8.4.1.
///
[Test]
public async Task RPRejectsUnencryptedSuggestion() {
Protocol protocol = Protocol.V20;
this.HandleProvider(
async (op, req) => {
// Receive initial request.
var request = await op.Channel.ReadFromRequestAsync(req, CancellationToken.None);
// Send a response that suggests a no encryption.
var renegotiateResponse = new AssociateUnsuccessfulResponse(request.Version, request);
renegotiateResponse.AssociationType = protocol.Args.SignatureAlgorithm.HMAC_SHA1;
renegotiateResponse.SessionType = protocol.Args.SessionType.NoEncryption;
return await op.Channel.PrepareResponseAsync(renegotiateResponse);
});
var rp = this.CreateRelyingParty();
var association = await rp.AssociationManager.GetOrCreateAssociationAsync(new ProviderEndpointDescription(OPUri, protocol.Version), CancellationToken.None);
Assert.IsNull(association, "The RP should quietly give up when the OP misbehaves.");
}
///
/// Verifies that the RP rejects an associate renegotiate request
/// when the HMAC and DH bit lengths do not match.
///
[Test]
public async Task RPRejectsMismatchingAssociationAndSessionBitLengths() {
Protocol protocol = Protocol.V20;
this.HandleProvider(
async (op, req) => {
// Receive initial request.
var request = await op.Channel.ReadFromRequestAsync(req, CancellationToken.None);
// Send a mismatched response
AssociateUnsuccessfulResponse renegotiateResponse = new AssociateUnsuccessfulResponse(request.Version, request);
renegotiateResponse.AssociationType = protocol.Args.SignatureAlgorithm.HMAC_SHA1;
renegotiateResponse.SessionType = protocol.Args.SessionType.DH_SHA256;
return await op.Channel.PrepareResponseAsync(renegotiateResponse);
});
var rp = this.CreateRelyingParty();
var association = await rp.AssociationManager.GetOrCreateAssociationAsync(new ProviderEndpointDescription(OPUri, protocol.Version), CancellationToken.None);
Assert.IsNull(association, "The RP should quietly give up when the OP misbehaves.");
}
///
/// Verifies that the RP cannot get caught in an infinite loop if a bad OP
/// keeps sending it association retry messages.
///
[Test]
public async Task RPOnlyRenegotiatesOnce() {
Protocol protocol = Protocol.V20;
int opStep = 0;
HandleProvider(
async (op, req) => {
switch (++opStep) {
case 1:
// Receive initial request.
var request = await op.Channel.ReadFromRequestAsync(req, CancellationToken.None);
// Send a renegotiate response
var renegotiateResponse = new AssociateUnsuccessfulResponse(request.Version, request);
renegotiateResponse.AssociationType = protocol.Args.SignatureAlgorithm.HMAC_SHA1;
renegotiateResponse.SessionType = protocol.Args.SessionType.DH_SHA1;
return await op.Channel.PrepareResponseAsync(renegotiateResponse, CancellationToken.None);
case 2:
// Receive second-try
request = await op.Channel.ReadFromRequestAsync(req, CancellationToken.None);
// Send ANOTHER renegotiate response, at which point the DNOI RP should give up.
renegotiateResponse = new AssociateUnsuccessfulResponse(request.Version, request);
renegotiateResponse.AssociationType = protocol.Args.SignatureAlgorithm.HMAC_SHA256;
renegotiateResponse.SessionType = protocol.Args.SessionType.DH_SHA256;
return await op.Channel.PrepareResponseAsync(renegotiateResponse, CancellationToken.None);
default:
throw Assumes.NotReachable();
}
});
var rp = this.CreateRelyingParty();
var association = await rp.AssociationManager.GetOrCreateAssociationAsync(new ProviderEndpointDescription(OPUri, protocol.Version), CancellationToken.None);
Assert.IsNull(association, "The RP should quietly give up when the OP misbehaves.");
}
///
/// Verifies security settings limit RP's acceptance of OP's counter-suggestion
///
[Test]
public async Task AssociateRenegotiateLimitedByRPSecuritySettings() {
Protocol protocol = Protocol.V20;
HandleProvider(
async (op, req) => {
op.SecuritySettings.MaximumHashBitLength = 160;
return await AutoProviderActionAsync(op, req, CancellationToken.None);
});
var rp = this.CreateRelyingParty();
rp.SecuritySettings.MinimumHashBitLength = 256;
var association = await rp.AssociationManager.GetOrCreateAssociationAsync(new ProviderEndpointDescription(OPUri, protocol.Version), CancellationToken.None);
Assert.IsNull(association, "No association should have been created when RP and OP could not agree on association strength.");
}
///
/// Verifies that the RP can recover from an invalid or non-existent
/// response from the OP, for example in the HTTP timeout case.
///
[Test]
public async Task AssociateQuietlyFailsAfterHttpError() {
// Without wiring up a mock HTTP handler, the RP will get a 404 Not Found error.
var rp = this.CreateRelyingParty();
var association = await rp.AssociationManager.GetOrCreateAssociationAsync(new ProviderEndpointDescription(OPUri, Protocol.V20.Version), CancellationToken.None);
Assert.IsNull(association);
}
///
/// Runs a parameterized association flow test using all supported OpenID versions.
///
/// The OP endpoint to simulate using.
private async Task ParameterizedAssociationTestAsync(Uri opEndpoint) {
foreach (Protocol protocol in Protocol.AllPracticalVersions) {
var endpoint = new ProviderEndpointDescription(opEndpoint, protocol.Version);
var associationType = protocol.Version.Major < 2 ? protocol.Args.SignatureAlgorithm.HMAC_SHA1 : protocol.Args.SignatureAlgorithm.HMAC_SHA256;
await this.ParameterizedAssociationTestAsync(endpoint, associationType);
}
}
///
/// Runs a parameterized association flow test.
///
///
/// The description of the Provider that the relying party uses to formulate the request.
/// The specific host is not used, but the scheme is significant.
///
///
/// The value of the openid.assoc_type parameter expected,
/// or null if a failure is anticipated.
///
private async Task ParameterizedAssociationTestAsync(
ProviderEndpointDescription opDescription,
string expectedAssociationType) {
Protocol protocol = Protocol.Lookup(Protocol.Lookup(opDescription.Version).ProtocolVersion);
bool expectSuccess = expectedAssociationType != null;
bool expectDiffieHellman = !opDescription.Uri.IsTransportSecure();
Association rpAssociation = null, opAssociation;
AssociateSuccessfulResponse associateSuccessfulResponse = null;
AssociateUnsuccessfulResponse associateUnsuccessfulResponse = null;
var relyingParty = new OpenIdRelyingParty(new MemoryCryptoKeyAndNonceStore(), this.HostFactories);
var provider = new OpenIdProvider(new MemoryCryptoKeyAndNonceStore(), this.HostFactories) {
SecuritySettings = this.ProviderSecuritySettings
};
Handle(opDescription.Uri).By(
async (request, ct) => {
IRequest req = await provider.GetRequestAsync(request, ct);
Assert.IsNotNull(req, "Expected incoming request but did not receive it.");
Assert.IsTrue(req.IsResponseReady);
return await provider.PrepareResponseAsync(req, ct);
});
relyingParty.Channel.IncomingMessageFilter = message => {
Assert.AreSame(opDescription.Version, message.Version, "The message was recognized as version {0} but was expected to be {1}.", message.Version, Protocol.Lookup(opDescription.Version).ProtocolVersion);
var associateSuccess = message as AssociateSuccessfulResponse;
var associateFailed = message as AssociateUnsuccessfulResponse;
if (associateSuccess != null) {
associateSuccessfulResponse = associateSuccess;
}
if (associateFailed != null) {
associateUnsuccessfulResponse = associateFailed;
}
};
relyingParty.Channel.OutgoingMessageFilter = message => {
Assert.AreEqual(opDescription.Version, message.Version, "The message was for version {0} but was expected to be for {1}.", message.Version, opDescription.Version);
};
relyingParty.SecuritySettings = this.RelyingPartySecuritySettings;
rpAssociation = await relyingParty.AssociationManager.GetOrCreateAssociationAsync(opDescription, CancellationToken.None);
if (expectSuccess) {
Assert.IsNotNull(rpAssociation);
Association actual = relyingParty.AssociationManager.AssociationStoreTestHook.GetAssociation(opDescription.Uri, rpAssociation.Handle);
Assert.AreEqual(rpAssociation, actual);
opAssociation = provider.AssociationStore.Deserialize(new TestSignedDirectedMessage(), false, rpAssociation.Handle);
Assert.IsNotNull(opAssociation, "The Provider could not decode the association handle.");
Assert.AreEqual(opAssociation.Handle, rpAssociation.Handle);
Assert.AreEqual(expectedAssociationType, rpAssociation.GetAssociationType(protocol));
Assert.AreEqual(expectedAssociationType, opAssociation.GetAssociationType(protocol));
Assert.IsTrue(Math.Abs(opAssociation.SecondsTillExpiration - rpAssociation.SecondsTillExpiration) < 60);
Assert.IsTrue(MessagingUtilities.AreEquivalent(opAssociation.SecretKey, rpAssociation.SecretKey));
if (expectDiffieHellman) {
Assert.IsInstanceOf(associateSuccessfulResponse);
var diffieHellmanResponse = (AssociateDiffieHellmanResponse)associateSuccessfulResponse;
Assert.IsFalse(MessagingUtilities.AreEquivalent(diffieHellmanResponse.EncodedMacKey, rpAssociation.SecretKey), "Key should have been encrypted.");
} else {
Assert.IsInstanceOf(associateSuccessfulResponse);
var unencryptedResponse = (AssociateUnencryptedResponse)associateSuccessfulResponse;
}
} else {
Assert.IsNull(relyingParty.AssociationManager.AssociationStoreTestHook.GetAssociation(opDescription.Uri, new RelyingPartySecuritySettings()));
}
}
}
}