wwauth/Google.Solutions.WWAuth/Adapters/Adfs/AdfsWsTrustAdapter.cs (110 lines of code) (raw):
//
// Copyright 2022 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Apis.Logging;
using Google.Apis.Util;
using Google.Solutions.WWAuth.Data;
using Google.Solutions.WWAuth.Data.Saml2;
using System;
using System.IdentityModel.Protocols.WSTrust;
using System.IdentityModel.Tokens;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.Threading;
using System.Threading.Tasks;
namespace Google.Solutions.WWAuth.Adapters.Adfs
{
/// <summary>
/// Adapter for acquiring tokens using WS-Trust.
/// </summary>
internal class AdfsWsTrustAdapter : AdfsAdapterBase
{
/// <summary>
/// AD FS Relying Party ID . Typed as string (and not Uri) to prevent
/// canonicaliuation, which might break the STS token exchange.
/// </summary>
public string RelyingPartyId { get; }
/// <summary>
/// SPN to verify in pre-flight checks.
/// </summary>
protected override string ServicePrincipalName
=> $"HOST/{this.IssuerUrl.Host}"; // WS-Trust needs host/, not http/!
public AdfsWsTrustAdapter(
Uri issuerUrl,
string relyingPartyId,
ILogger logger) : base(issuerUrl, logger)
{
if (!Uri.IsWellFormedUriString(relyingPartyId, UriKind.Absolute))
{
throw new ArgumentException("Relying party ID must be a URI");
}
this.RelyingPartyId = relyingPartyId.ThrowIfNull(nameof(relyingPartyId));
}
/// <summary>
/// Create a WS-Trust channel factory that uses the given credential
/// to authenticate to AD FS.
/// </summary>
private WSTrustChannelFactory CreateChannelFactory()
{
var binding = new WS2007HttpBinding(SecurityMode.TransportWithMessageCredential);
binding.Security.Message.EstablishSecurityContext = false;
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
//
// Use Integrated Windows Authentication (IWA).
//
binding.Security.Message.ClientCredentialType = MessageCredentialType.Windows;
var factory = new WSTrustChannelFactory(
binding,
new EndpointAddress(
new Uri(this.IssuerUrl, "services/trust/13/windowsmixed")))
{
TrustVersion = TrustVersion.WSTrust13
};
factory.Credentials.Windows.ClientCredential = this.Credential;
return factory;
}
/// <summary>
/// Use WS-Trust to obtain a SAML assertion from AD FS.
/// </summary>
private async Task<GenericXmlSecurityToken> AcquireSamlSecurityTokenAsync(
WSTrustChannelFactory factory)
{
//
// Request a SAML 2.0 assertion (as opposed to SAML 1.1, which is
// the default for WS-Trust).
//
var tokenRequest = new RequestSecurityToken
{
RequestType = RequestTypes.Issue,
AppliesTo = new EndpointReference(this.RelyingPartyId),
KeyType = KeyTypes.Bearer,
TokenType = "urn:oasis:names:tc:SAML:2.0:assertion"
};
var channel = factory.CreateChannel();
try
{
this.Logger.Info(
"Acquiring SAML assertion for {0} and relying party {1} using WS-Trust",
factory.Credentials.UserName,
factory.Endpoint.Address);
return await Task.Factory.FromAsync(
channel.BeginIssue(tokenRequest, null, null),
ar => (GenericXmlSecurityToken)channel.EndIssue(ar, out var _));
}
catch (SecurityNegotiationException e)
{
throw new TokenAcquisitionException(
"Authentication failed. " +
"If AD FS is deployed behind a load balancer, verify that the " +
"token binding settings (ExtendedProtectionTokenCheck) are compatible " +
"with your load balancer setup.", e);
}
catch (FaultException e) when (
e.Code != null &&
e.Code.IsSenderFault &&
e.Code.SubCode.Name == "InvalidScope")
{
throw new TokenAcquisitionException(
$"The relying party ID '{this.RelyingPartyId}' " +
"is invalid or does not exist", e);
}
catch (FaultException e) when (
e.Code != null &&
e.Code.IsSenderFault &&
e.Code.SubCode.Name == "FailedAuthentication" &&
factory.Credentials.UserName?.UserName != null)
{
throw new TokenAcquisitionException(
"Authentication failed, verify that the credentials " +
$"for {factory.Credentials.UserName.UserName} are correct", e);
}
catch (Exception e)
{
this.Logger.Error(e, "Acquiring assertion failed: {0}", e.Message);
throw;
}
}
protected override async Task<ISubjectToken> AcquireTokenCoreAsync(
CancellationToken cancellationToken)
{
var samlToken = await AcquireSamlSecurityTokenAsync(
CreateChannelFactory())
.ConfigureAwait(false);
return new Assertion(samlToken);
}
}
}