//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.Test.OpenId {
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using DotNetOpenAuth.Logging;
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 MemoryCryptoKeyAndNonceStore();
Handle(RPUri).By(
async req => {
var rp = new OpenIdRelyingParty(new MemoryCryptoKeyAndNonceStore(), this.HostFactories);
IAuthenticationResponse response = await rp.GetResponseAsync(req);
Assert.That(response, Is.Not.Null);
Assert.AreEqual(AuthenticationStatus.Authenticated, response.Status);
return new HttpResponseMessage();
});
Handle(OPUri).By(
async (req, ct) => {
var op = new OpenIdProvider(opStore, this.HostFactories);
return await this.AutoProviderActionAsync(op, req, ct);
});
this.RegisterMockRPDiscovery(ssl: false);
{
var op = new OpenIdProvider(opStore, this.HostFactories);
Identifier id = GetMockIdentifier(ProtocolVersion.V20);
var assertion = await op.PrepareUnsolicitedAssertionAsync(OPUri, RPRealmUri, id, OPLocalIdentifiers[0]);
using (var httpClient = this.HostFactories.CreateHttpClient()) {
using (var response = await httpClient.GetAsync(assertion.Headers.Location)) {
response.EnsureSuccessStatusCode();
}
}
}
}
[Test]
public async Task UnsolicitedAssertionRejected() {
var opStore = new MemoryCryptoKeyAndNonceStore();
Handle(RPUri).By(
async req => {
var rp = new OpenIdRelyingParty(new MemoryCryptoKeyAndNonceStore(), this.HostFactories);
rp.SecuritySettings.RejectUnsolicitedAssertions = true;
IAuthenticationResponse response = await rp.GetResponseAsync(req);
Assert.That(response, Is.Not.Null);
Assert.AreEqual(AuthenticationStatus.Failed, response.Status);
return new HttpResponseMessage();
});
Handle(OPUri).By(
async req => {
var op = new OpenIdProvider(opStore, this.HostFactories);
return await this.AutoProviderActionAsync(op, req, CancellationToken.None);
});
this.RegisterMockRPDiscovery(ssl: false);
{
var op = new OpenIdProvider(opStore, this.HostFactories);
Identifier id = GetMockIdentifier(ProtocolVersion.V20);
var assertion = await op.PrepareUnsolicitedAssertionAsync(OPUri, RPRealmUri, id, OPLocalIdentifiers[0]);
using (var httpClient = this.HostFactories.CreateHttpClient()) {
using (var response = await httpClient.GetAsync(assertion.Headers.Location)) {
response.EnsureSuccessStatusCode();
}
}
}
}
///
/// Verifies that delegating identifiers are rejected in unsolicited assertions
/// when the appropriate security setting is set.
///
[Test]
public async Task UnsolicitedDelegatingIdentifierRejection() {
var opStore = new MemoryCryptoKeyAndNonceStore();
Handle(RPUri).By(
async req => {
var rp = this.CreateRelyingParty();
rp.SecuritySettings.RejectDelegatingIdentifiers = true;
IAuthenticationResponse response = await rp.GetResponseAsync(req);
Assert.That(response, Is.Not.Null);
Assert.AreEqual(AuthenticationStatus.Failed, response.Status);
return new HttpResponseMessage();
});
Handle(OPUri).By(
async req => {
var op = new OpenIdProvider(opStore, this.HostFactories);
return await this.AutoProviderActionAsync(op, req, CancellationToken.None);
});
this.RegisterMockRPDiscovery(ssl: false);
{
var op = new OpenIdProvider(opStore, this.HostFactories);
Identifier id = GetMockIdentifier(ProtocolVersion.V20, false, true);
var assertion = await op.PrepareUnsolicitedAssertionAsync(OPUri, RPRealmUri, id, OPLocalIdentifiers[0]);
using (var httpClient = this.HostFactories.CreateHttpClient()) {
using (var response = await httpClient.GetAsync(assertion.Headers.Location)) {
response.EnsureSuccessStatusCode();
}
}
}
}
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;
HandleProvider(
async (op, req) => {
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, CancellationToken.None);
Assert.IsNotNull(request);
IProtocolMessage response;
if (positive) {
response = new PositiveAssertionResponse(request);
} else {
response = await NegativeAssertionResponse.CreateAsync(request, CancellationToken.None, op.Channel);
}
return await op.Channel.PrepareResponseAsync(response);
case 2:
if (positive && (statelessRP || !sharedAssociation)) {
var checkauthRequest =
await op.Channel.ReadFromRequestAsync(req, CancellationToken.None);
var checkauthResponse = new CheckAuthenticationResponse(checkauthRequest.Version, checkauthRequest);
checkauthResponse.IsValid = checkauthRequest.IsValid;
return await op.Channel.PrepareResponseAsync(checkauthResponse);
}
throw Assumes.NotReachable();
case 3:
if (positive && (statelessRP || !sharedAssociation)) {
if (!tamper) {
// Respond to the replay attack.
var checkauthRequest =
await op.Channel.ReadFromRequestAsync(req, CancellationToken.None);
var checkauthResponse = new CheckAuthenticationResponse(checkauthRequest.Version, checkauthRequest);
checkauthResponse.IsValid = checkauthRequest.IsValid;
return await op.Channel.PrepareResponseAsync(checkauthResponse);
}
}
throw Assumes.NotReachable();
default:
throw Assumes.NotReachable();
}
});
{
var rp = this.CreateRelyingParty(statelessRP);
if (tamper) {
rp.Channel.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";
}
};
}
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);
Uri redirectResponse;
this.HostFactories.AllowAutoRedirects = false;
using (var httpClient = rp.Channel.HostFactories.CreateHttpClient()) {
using (var response = await httpClient.GetAsync(redirectRequest.Headers.Location)) {
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
redirectResponse = response.Headers.Location;
}
}
var assertionMessage = new HttpRequestMessage(HttpMethod.Get, redirectResponse);
if (positive) {
if (tamper) {
try {
await rp.Channel.ReadFromRequestAsync(assertionMessage, CancellationToken.None);
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, CancellationToken.None);
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 {
await rp.Channel.ReadFromRequestAsync(assertionMessage, CancellationToken.None);
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, CancellationToken.None);
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);
}
}
}
}
}
}