we have successfully authenticated to ADFS 3.0 using OAuth using a custom TokenValidationHandler.
public class TokenValidationHandler : DelegatingHandler
{
private const string JwtAccessTokenCookieName = "jwt_access_token";
private static readonly string adfsUrl = ConfigurationManager.AppSettings["oauth2.adfsUrl"];
private static readonly string clientId = ConfigurationManager.AppSettings["oauth2.clientId"];
private static readonly string redirectUrl = ConfigurationManager.AppSettings["oauth2.redirectUrl"];
private static readonly string rptIdentifier = ConfigurationManager.AppSettings["oauth2.relyingPartyTrustIdentifier"];
private AdfsMetadata adfsMetaData;
public TokenValidationHandler()
{
string stsMetadataAddress = string.Format(CultureInfo.InvariantCulture, $"{adfsUrl}/federationmetadata/2007-06/federationmetadata.xml");
adfsMetaData = new AdfsMetadata(stsMetadataAddress);
}
// SendAsync is used to validate incoming requests contain a valid access token, and sets the current user identity
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
using (HttpResponseMessage responseMessage = new HttpResponseMessage())
{
string jwtToken;
if (HasNoJWTAccessToken(request, out jwtToken))
{
string authorizationCode;
if (HasNoAuthorizationCode(request, out authorizationCode))
{
return RedirectToADFSLoginScreen(request);
}
var responseTokenAsJson = await GetAccessToken(cancellationToken, authorizationCode);
return RedirectToAppWithAccessTokenInCookie(request, responseTokenAsJson);
}
try
{
var tokenHandler = new JwtSecurityTokenHandler { TokenLifetimeInMinutes = 60 };
var validationParameters = new TokenValidationParameters
{
ValidIssuer = adfsMetaData.Issuer,
IssuerSigningKeys = adfsMetaData.SigningTokens.Select(token => new X509SecurityKey(token.Certificate)),
ValidateAudience = false,
SaveSigninToken = true
};
try
{
Microsoft.IdentityModel.Tokens.SecurityToken valdidationtoken;
// Validate token
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwtToken, validationParameters, out valdidationtoken);
//set the ClaimsPrincipal on the current thread.
Thread.CurrentPrincipal = claimsPrincipal;
if (HttpContext.Current != null)
{
HttpContext.Current.Items["jwtTokenAsString"] = jwtToken;
HttpContext.Current.Items["jwtTokenAsSecurityToken"] = valdidationtoken;
HttpContext.Current.User = claimsPrincipal;
}
return await base.SendAsync(request, cancellationToken);
}
catch (Exception exception)
{
responseMessage.StatusCode = HttpStatusCode.Unauthorized;
return new HttpResponseMessage(HttpStatusCode.Unauthorized)
{
Content = new StringContent(exception.Message)
};
}
}
catch (Exception w)
{
return new HttpResponseMessage(HttpStatusCode.InternalServerError)
{
Content = new StringContent(w.Message)
};
}
}
}
private static async Task<JObject> GetAccessToken(CancellationToken cancellationToken, string authorizationCode)
{
ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
HttpClient httpClient = new HttpClient();
var httpResponseMessage = await httpClient.PostAsync(new Uri($"{adfsUrl}/adfs/oauth2/token"), GenerateTokenRequestContent(authorizationCode), cancellationToken);
var responseContent = await httpResponseMessage.Content.ReadAsStringAsync();
JObject responseToken = JObject.Parse(responseContent);
return responseToken;
}
private static HttpResponseMessage RedirectToADFSLoginScreen(HttpRequestMessage request)
{
var requestUriAsString = request.RequestUri.ToString();
var redirectResponse = new HttpResponseMessage(HttpStatusCode.Moved);
redirectResponse.Headers.Location =
new Uri($"{adfsUrl}/adfs/oauth2/authorize?response_type=code&client_id={clientId}&redirect_uri={HttpUtility.UrlEncode(redirectUrl)}&resource={HttpUtility.UrlEncode(rptIdentifier)}&state={GZipUtils.Compress(requestUriAsString)}");
return redirectResponse;
}
private static HttpResponseMessage RedirectToAppWithAccessTokenInCookie(HttpRequestMessage request, JObject responseTokenAsJson)
{
var cookie = CreateCookieWithAccessToken(request, responseTokenAsJson);
var urlToRedirectTo = GZipUtils.Decompress(request.GetQueryNameValuePairs().FirstOrDefault(param => param.Key == "state").Value);
var redirectResponse = new HttpResponseMessage(HttpStatusCode.Redirect);
redirectResponse.Headers.Location = new Uri(urlToRedirectTo);
redirectResponse.Headers.AddCookies(new[] { cookie });
return redirectResponse;
}
private static CookieHeaderValue CreateCookieWithAccessToken(HttpRequestMessage request, JObject responseTokenAsJson)
{
var compressedToken = GZipUtils.Compress(responseTokenAsJson["access_token"].ToString());
var cookie = new CookieHeaderValue(JwtAccessTokenCookieName, compressedToken)
{
Expires = DateTimeOffset.Now.AddSeconds(Int16.Parse(responseTokenAsJson["expires_in"].ToString())),
Domain = request.RequestUri.Host,
Path = "/"
};
return cookie;
}
private static FormUrlEncodedContent GenerateTokenRequestContent(string authorizationCode)
{
return new FormUrlEncodedContent(
new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("grant_type","authorization_code"),
new KeyValuePair<string, string>("client_id", clientId),
new KeyValuePair<string, string>("code", authorizationCode),
new KeyValuePair<string, string>("redirect_uri", redirectUrl),
});
}
private bool HasNoAuthorizationCode(HttpRequestMessage request, out string authorizationCode)
{
authorizationCode = request.GetQueryNameValuePairs().FirstOrDefault(param => param.Key == "code").Value;
return string.IsNullOrEmpty(authorizationCode);
}
// Reads the token from the authorization header on the incoming request
static bool HasNoJWTAccessToken(HttpRequestMessage request, out string token)
{
if (HasNoJWTAccessTokenInAuthorizationHeader(request, out token) && HasNoJWTAccessTokenInSecureCookie(request, out token))
{
return true;
}
return false;
}
private static bool HasNoJWTAccessTokenInSecureCookie(HttpRequestMessage request, out string token)
{
token = null;
if (!request.Headers.GetCookies(JwtAccessTokenCookieName).Any())
{
return true;
}
var cookieHeaderValue = request.Headers.GetCookies(JwtAccessTokenCookieName).FirstOrDefault();
if (cookieHeaderValue != null)
{
token = GZipUtils.Decompress(cookieHeaderValue[JwtAccessTokenCookieName].Value);
}
if (token == null)
{
return true;
}
return false;
}
private static bool HasNoJWTAccessTokenInAuthorizationHeader(HttpRequestMessage request, out string token)
{
token = null;
if (!request.Headers.Contains("Authorization"))
{
return true;
}
string authHeader = request.Headers.GetValues("Authorization").FirstOrDefault();
// Verify Authorization header contains 'Bearer' scheme
token = authHeader.StartsWith("Bearer ", StringComparison.Ordinal) ? authHeader.Split(' ')[1] : null;
if (token == null)
{
return true;
}
return false;
}
}
Note: this is still a work in progress (that's why we disable ssl validation).
Now we need to transform this JWT token to a SAML token for some WCF services. IMPORTANT: we cannot change anything to the WCF services as they are not under our control. This means this solution is not applicable for us: How to use JWT tokens with WCF and WIF?
I have access to the original JWT token via the bootstrapcontext.
ClaimsPrincipal principal = (ClaimsPrincipal) Thread.CurrentPrincipal;
var bootstrapContext = principal.Identities.First().BootstrapContext; //=> contains original JWT token.
System.IdentityModel.Tokens.SecurityToken token;
var rstr = RequestSecurityToken(out token); // => need help here
var channelFactory = new ChannelFactory<T>(endpointConfigurationName);
return channelFactory.CreateChannelWithActAsToken(token);
What would be the best approach to do so?
The current configuration to go to WCF (which we received from the other party and is not under our control) is as follows:
<security authenticationMode="IssuedTokenOverTransport" messageSecurityVersion="WSSecurity11WSTrust13WSSecureConversation13WSSecurityPolicy12BasicSecurityProfile10">
<issuedTokenParameters keyType="SymmetricKey" tokenType="http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0">
<issuer address="https://fs.contoso-int.be/adfs/services/trust/13/kerberosmixed" binding="customBinding" bindingConfiguration="Contoso.Federation.Bindings.Http.KerberosMixed">
<identity>
<servicePrincipalName value="host/fs.contoso-int.be" />
</identity>
</issuer>
<issuerMetadata address="https://fs.contoso-int.be/adfs/services/trust/mex" />
<claimTypeRequirements>
<add claimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
<add claimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" isOptional="true" />
</claimTypeRequirements>
<additionalRequestParameters>
<trust:TokenType xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0</trust:TokenType>
<trust:KeyType xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey</trust:KeyType>
<trust:KeySize xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">256</trust:KeySize>
<trust:KeyWrapAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p</trust:KeyWrapAlgorithm>
<trust:EncryptWith xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptWith>
<trust:SignWith xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2000/09/xmldsig#hmac-sha1</trust:SignWith>
<trust:CanonicalizationAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/10/xml-exc-c14n#</trust:CanonicalizationAlgorithm>
<trust:EncryptionAlgorithm xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">http://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptionAlgorithm>
</additionalRequestParameters>
</issuedTokenParameters>
<localClientSettings detectReplays="false" />
<localServiceSettings detectReplays="false" />
</security>
I already tried to create a SAML token via a RequestSecurityToken but the moment I add the ActAs SecurityTokenElement, I receive an InvalidSecurityToken from ADFS.
The Soap-enveloppe to request the SAML token is as follows:
<?xml version="1.0"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<a:Action s:mustUnderstand="1">http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue</a:Action>
<a:MessageID>urn:uuid:64f34b8a-92bf-4da0-9571-d436ab24d5d1</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<a:To s:mustUnderstand="1">https://fs.contoso-int.be/adfs/services/trust/13/kerberosmixed</a:To>
<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
<u:Timestamp u:Id="_0">
<u:Created>2016-12-23T15:11:28.885Z</u:Created>
<u:Expires>2016-12-23T15:16:28.885Z</u:Expires>
</u:Timestamp>
<o:BinarySecurityToken u:Id="uuid-abcb8b3a-61e0-4c9d-a6f3-71ad407b838d-1" ValueType="http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">YIIGmgYJKoZIhvcSAQICAQB...</o:BinarySecurityToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"/>
<Reference URI="#_0">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>1qIxIurrORfpzYMl3AHVmVNGJ9Y=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>bCacOSkpjauc+QpMbUqCQ/aQE20=</SignatureValue>
<KeyInfo>
<o:SecurityTokenReference>
<o:Reference URI="#uuid-abcb8b3a-61e0-4c9d-a6f3-71ad407b838d-1"/>
</o:SecurityTokenReference>
</KeyInfo>
</Signature>
</o:Security>
</s:Header>
<s:Body>
<trust:RequestSecurityToken xmlns:trust="http://docs.oasis-open.org/ws-sx/ws-trust/200512">
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>urn:co:feat</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<trust:KeyType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey</trust:KeyType>
<tr:ActAs xmlns:tr="http://docs.oasis-open.org/ws-sx/ws-trust/200802">
<wsse:BinarySecurityToken xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" ValueType="urn:ietf:params:oauth:token-type:jwt" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">ZXlKMGVYQWlPaUpLVjFRaUxDSmhi...</wsse:BinarySecurityToken>
</tr:ActAs>
<trust:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</trust:RequestType>
</trust:RequestSecurityToken>
</s:Body>
</s:Envelope>