I am experiencing a weird issue when exchanging a OAuth access token to a SAML Assertion using Azure AD and On-Behalf-Of Flow. I am trying to exchange a OAuth access token to a SAML Assertion using the On-Behalf-Of flow of Azure AD.
Setup
The request to fetch data from the datasource needs to be performed from the Back-End since there are access restrictions to the datasource in place.
Description
Following the documentation for Azure AD v1 (Github docs), I was able to request a response which initially looks fine. The parameters for the request I used are:
grant_type: urn:ietf:params:oauth:grant-type:jwt-bearer
assertion: <access token containing the correct scopes for the Back-End>
client_id: <client-id-of-back-end>
client_secret: <assigned-secret>
resource: <resource-of-the-datasource>
requested_token_use: on_behalf_of
requested_token_type: urn:ietf:params:oauth:token-type:saml2
The request is sent as POST request, using x-www-form-urlencoded
as content-type (endpoint "https://login.microsoftonline.com/tenant-id/oauth2/token").
Issue
I am almost certain, I am encountering a bug, however I did not figure out how to contact Azure without having a Developer Support Plan. The response I receive looks fine at first:
{
"token_type": "Bearer",
"expires_in": "3579",
"ext_expires_in": "3579",
"expires_on": "1613985579",
"resource": "<datasource>",
"access_token": "PEFzc2Vyd...9uPg",
"issued_token_type": "urn:ietf:params:oauth:token-type:saml2",
"refresh_token": "0.ATEAt...hclkg-7g"
}
The assertion from the access_token
field is not a valid base64 string. Trying to decode it using C# Base64Convert
, results in this exception:
System.FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
I was however able to partly decode it using bashs base64 -D
, which gave me a somehow valid assertion:
$ base64 -D "response.txt"
Invalid character in input stream.
<Assertion ID="_26be6964-2e17-4184-8ac7-d4cdbb9d5700" IssueInstant="2021-02-22T12:35:49.919Z" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion"><Issuer>https://sts.windows.net/[id]/</Issuer><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/2001/04/xmldsig-more#rsa-sha256"/><Reference URI="#_26be6964-2e17-4184-8ac7-d4cdbb9d5700"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>...<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"><AttributeValue>[email protected]</
Question
I am almost sure, that the assertion should be a valid base64 string to decode using anything capable of doing so. Am I missing something? Or is this a known issue with V1 OBO Flow? Is there a known workaround for this?
the assertion is the access-token that you receive in the initial call to AAD as mentioned here.
This is a JWT token which is Based64 URL encoded and can be decoded using tools like https://JWT.io or https://JWT.ms or using any programming language too. The main point is that if the access-token issued is a valid access-token, it should get decoded, and that's the same access-token that gets added in the subsequent call to fetch a SAML token.
You can also check the following article that speaks on OBO flow: https://blogs.aaddevsup.xyz/2019/08/understanding-azure-ads-on-behalf-of-flow-aka-obo-flow/
The main point to note here would be how we are requesting the initial Access-token from AAD. If your Front-end is a SPA and if you are using Implicit Flow there, you might want to take a look at this "As of May 2018, some implicit-flow derived id_token can't be used for OBO flow. Single-page apps (SPAs) should pass an access token to a middle-tier confidential client to perform OBO flows instead."
While decoding a JWT, it first needs to be converted to a Base64 encoded string from a Base64URL encoded string. Once the JWT is base64 encoded, then it needs to be decoded and later on parse that into json.
A Powershell Sample for the same:
$token = "<put the jwt here>"
if (!$token.Contains(".") -or !$token.StartsWith("eyJ")) {
Write-Error "Invalid token" -ErrorAction Stop
}
# Token
foreach ($i in 0..1) {
$data = $token.Split('.')[$i].Replace('-', '+').Replace('_', '/')
switch ($data.Length % 4) {
0 { break }
2 { $data += '==' }
3 { $data += '=' }
}
}
$decodedToken = [System.Text.Encoding]::UTF8.GetString([convert]::FromBase64String($data)) | ConvertFrom-Json
Write-Verbose "JWT Token:"
Write-Verbose $decodedToken
C# sample:
static void jwtDecoder()
{
try
{
Console.WriteLine("JWT to Decode: " + jwtEncodedString + "\n");
var jwtHandler = new JwtSecurityTokenHandler();
var readableToken = jwtHandler.CanReadToken(jwtEncodedString);
if (readableToken != true)
{
Console.WriteLine("\n\nThe token doesn't seem to be in a proper JWT format.\n\n");
}
if (readableToken == true)
{
var token = jwtHandler.ReadJwtToken(jwtEncodedString);
var headers = token.Header;
var jwtHeader = "{";
foreach (var h in headers)
{
jwtHeader += '"' + h.Key + "\":\"" + h.Value + "\",";
}
jwtHeader += "}";
Console.Write("\nHeader :\r\n" + JToken.Parse(jwtHeader).ToString(Formatting.Indented));
var claims = token.Claims;
var jwtPayLoad = "{";
foreach (Claim c in claims)
{
jwtPayLoad += '"' + c.Type + "\":\"" + c.Value + "\",";
}
jwtPayLoad += "}";
Console.Write("\r\nPayload :\r\n" + JToken.Parse(jwtPayLoad).ToString(Formatting.Indented));
var jwtSignature = "[RawSignature: ";
jwtSignature += token.RawSignature;
jwtSignature += " ]";
Console.Write("\r\nSignature :\r\n" + jwtSignature);
//Console.ReadLine();
}
}
finally
{
Console.Write("\n\nPress Enter to close window ...");
Console.Read();
}
}