Search code examples
azureoauth-2.0azure-active-directorysaml-2.0

Invalid Base64 SAML Assertion from OAuth on-behalf-of flow in Azure AD


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

  • A Front-End communicating with a Back-End using OAuth access tokens
  • A datasource which I need to get data from, which is protected with SAML

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?


Solution

  • 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();
                }
            }