AzureAD / azure-activedirectory-identitymodel-extensions-for-dotnet

IdentityModel extensions for .Net
MIT License
1.05k stars 399 forks source link

Cannot validate signature. #609

Closed jvandervelden closed 7 years ago

jvandervelden commented 7 years ago

I am trying to use this library to validate the tokens I receive from our UI. The UI gets it from a login web app that is registered with Azure AD.

I have setup a simple application that takes a token and tries to validate the signature found here: https://github.com/jvandervelden/test-azure-ad-identiymodel-connect The class that's doing the validation is here: https://github.com/jvandervelden/test-azure-ad-identiymodel-connect/blob/master/TestStuff/TokenValidator.cs

The error I am getting is: 'IDX10503: Signature validation failed. Keys tried: 'Microsoft.IdentityModel.Tokens.X509SecurityKey , KeyId: z039zdsFuizpBfBVK1Tn25QHYO0'

Simple application with the error:

Any help would be appreciated. Using NuGet packages: Microsoft.IdentityModel.Protocols v2.1.3 Microsoft.IdentityModel.Protocols v2.1.3 Microsoft.IdentityModel.Tokens v5.1.3 System.IdentityModel.Tokens.Jwt v5.1.3
brentschmaltz commented 7 years ago

@jvandervelden is there a possibility you can share the 'jwt'. You can email me directly if it contains sensitive info.

jvandervelden commented 7 years ago

@brentschmaltz No Problem, it's in the image anyway, and it's a test tenancy.

deleted this token as I now understand it's purpose

brentschmaltz commented 7 years ago

@jvandervelden I just ran this through validation. _rsa.VerifyData(input, signature, _hashAlgorithm, RSASignaturePadding.Pkcs1); returns false.

https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/Microsoft.IdentityModel.Tokens/AsymmetricSignatureProvider.cs#L358

Called from: https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs#L911

How did you obtain that token?

jvandervelden commented 7 years ago

@brentschmaltz We have a custom web app registered with our AD that does logins. When you request a login from our app it redirects you to the login.microsoftonline.com page: (https://login.microsoftonline.com/common/oauth2/authorize?response_type=code%20id_token&scope=openid&response_mode=form_post&redirect_uri=http%3A%2F%2Flocalhost%3A8089%2Fsecure%2Flogin&client_id=409e2f16-9ace-4e18-acc3-4c7f6a6fba3a&resource=https://graph.microsoft.com/&nonce=da3d8159-f9f6-4fa8-bbf8-9a2cd108a261&site_id=500879). This posts pack and access code we then use to get a token from: (https://login.microsoftonline.com/common/oauth2/token) which then gives us back the JWT token that I posted.

We use the ADAL for java library to get the token.

brentschmaltz commented 7 years ago

@jvandervelden OK that explains it then. I missed it before, but if look at the Jwt.Header you will see a 'nonce'. This means you need special processing. Normal processing will fail.

jvandervelden commented 7 years ago

@brentschmaltz So what's your suggestion? Should I remove the nonce from the login url? We are going to attempt to validate these in nginx lua and I don't really want to add extra pain.

brentschmaltz commented 7 years ago

@jvandervelden i need to understand your topology better. Also what scenario are you trying to implement? The token you have is for graph, why are you trying to validate it?

brentschmaltz commented 7 years ago

@jvandervelden I am going to close this as not supported.

jvandervelden commented 7 years ago

@brentschmaltz Here (https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-token-and-claims) states I should be able to validate the id_token and the access_token the same way. When I get to this section (https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-token-and-claims#validating-tokens) it gives an example project for manually validating tokens (https://github.com/Azure-Samples/active-directory-dotnet-webapi-manual-jwt-validation). I ran this project and gave it an id_token which worked, I then gave it my access token which failed with the same error I was seeing.

I tried updating my login url so I didn't have to provide an 'NONCE' parameter which was fine, I got the access_code and sent that on to the token endpoint. No matter what I do the 'NONCE' value is always in the header of my access_token:

Getting the access_token: image

Token: eyJ0eXAiOiJKV1QiLCJub25jZSI6IkFRQUJBQUFBQUFCbmZpRy1tQTZOVGFlN0NkV1c3UWZkcl92QTF0ZF9LSjU1dWV3elY4dUEtc0dPaXBSS3puUEZQenZxNTVmUDhlaTFDcm95Qk9HWThWVjdtTVhJNndKUlNNbkxUUGxPdlNValV2NFlvLUswX3lBQSIsImFsZyI6IlJTMjU2IiwieDV0IjoiejAzOXpkc0Z1aXpwQmZCVksxVG4yNVFIWU8wIiwia2lkIjoiejAzOXpkc0Z1aXpwQmZCVksxVG4yNVFIWU8wIn0.eyJhdWQiOiJodHRwczovL2dyYXBoLm1pY3Jvc29mdC5jb20vIiwiaXNzIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvNWFmYTgwYjUtN2FjNS00MWY3LWIxZjktMDU4MDE5NWIwZDk5LyIsImlhdCI6MTQ5NDI1NTY0MCwibmJmIjoxNDk0MjU1NjQwLCJleHAiOjE0OTQyNTk1NDAsImFjciI6IjEiLCJhaW8iOiJZMlpnWUxpVXZuYnFkcjM5YWVKYzJtR01MaGRtSjdQY0tlallGSk84NVZlZHd1c1pMaDBBIiwiYW1yIjpbInB3ZCJdLCJhcHBfZGlzcGxheW5hbWUiOiJUZXN0QyMiLCJhcHBpZCI6IjQwOWUyZjE2LTlhY2UtNGUxOC1hY2MzLTRjN2Y2YTZmYmEzYSIsImFwcGlkYWNyIjoiMSIsImlwYWRkciI6IjY0LjIzNS4xMDAuNiIsIm5hbWUiOiJKYXNwZXIgVXB0dXJuIiwib2lkIjoiMGZlNzE4ZjQtNWUxYS00MDI5LTk4MmQtZjU0YjQxZTRkMzA2IiwicGxhdGYiOiIzIiwicHVpZCI6IjEwMDM3RkZFQTBDNEZBMkEiLCJzY3AiOiJEaXJlY3RvcnkuQWNjZXNzQXNVc2VyLkFsbCBVc2VyLlJlYWQiLCJzdWIiOiJKcl80X2U5cnRCNS0zZUdtbmxjMmhqWEpyNXZHMVRoUmZyLXhOeHhSR3drIiwidGlkIjoiNWFmYTgwYjUtN2FjNS00MWY3LWIxZjktMDU4MDE5NWIwZDk5IiwidW5pcXVlX25hbWUiOiJqYXNwZXJAdmFuZGVydmVsZGVucy5jYSIsInVwbiI6Imphc3BlckB2YW5kZXJ2ZWxkZW5zLmNhIiwidXRpIjoiRDFqanZXUGVQMDZLZElKSGdPUS1BQSIsInZlciI6IjEuMCIsIndpZHMiOlsiNjJlOTAzOTQtNjlmNS00MjM3LTkxOTAtMDEyMTc3MTQ1ZTEwIl19.ARoy6EBl1UM1U3_uStikPvOND-Y7otrZxVvyunAMrV6D1Wga1CIJJ5UxFpUE-MAwdebj1Pv_YsQ9o3Ys3i9qroAYoiJUDtycqW0iX1sNeoCndm147K3OlcnIQpcw2zupxOFCkecV_kmVroQGTcod4Gtni3P2uTdmeGAJABgf0wa6D0fplYd_GLwkuz1QAXjHe6LsmGwj-5iG7Oquy1mLTEZ4ptyaGSiM5Bv9GGXK1ZyJrDyJVV06cX-VbHF_EiKZlHFzH2OC8hFgyjdP5hPmQAaeSeFPCIir1qEhhSwIIzl7lzDJzNJjU5_dH88QBgA2NVcN8uWI-QniBvr6GrBG3A

Header: { "typ": "JWT", "nonce": "AQABAAAAAABnfiG-mA6NTae7CdWW7Qfdr_vA1td_KJ55uewzV8uA-sGOipRKznPFPzvq55fP8ei1CroyBOGY8VV7mMXI6wJRSMnLTPlOvSUjUv4Yo-K0_yAA", "alg": "RS256", "x5t": "z039zdsFuizpBfBVK1Tn25QHYO0", "kid": "z039zdsFuizpBfBVK1Tn25QHYO0" }

So what we are doing is, we have a login web app that handles the authentication portion of our system. The user hits 'https://login.ourwebapp.com/secure/login?webApp=https://cloud.ourwebapp.com/'. This redirects them to 'https://login.microsoftonline.com?redirect=https://login.ourwebapp.com&client_id...'. Once the user logins to the azure portal it posts back the access_token to the login webapp. The login web app then uses the ADAL4j library to get the access_token using the access_code provided. It then redirects the user to the webApp url passed in the initial request with the access_token. The second webApp then utilizes this token to validate the user is authenticated and can access it's data. This second web app is where we need to validate the token to make sure the user is authenticated.

jvandervelden commented 7 years ago

@brentschmaltz thank you for your help, I figured out my issue. I was trying to validate an access token that was for the graph api. I created an access token for my web app and I can validate the token now.

chetanku commented 6 years ago

Hello, How was this resolved? I have an access token that I receive from Graph API. I am trying to validate it with the key found in the https://login.microsoftonline.com/common/discovery/keys. I am using jwt.io. But it fails with Invalid Signature. The token has nonce value. What kind of special processing is required? My goal is to send the token in headers to my api and get the token validated using jwt token validation method. Any help will be great, stuck in this since past few days.

brentschmaltz commented 6 years ago

@chetanku a token from Graph that has a 'nonce' requires special processing to validate the signature

chetanku commented 6 years ago

@brentschmaltz Do we have any documentation around special processing? Is there anyway we can remove the nonce from the header? One more question: Can we use the access token we get from 'https://login.microsoftonline.com/tenantID/oauth2/v2.0/authorize' for securing back end(validation from backend API)? or do we need id_token in response? Also, I read in some articles that the aud in the header needs to be changed to your API. Currently, it is https://graph.microsoft.com

Thanks for your help.

rjurado01 commented 6 years ago

I have the same error and it was due to I have setting into scope field of token request (doc) the graph app uri: https://graph.microsoft.com/.default.

I have change it for my App ID URI adding /.default and it works.

lokori commented 6 years ago

It would be very interesting to hear what this "special processing" means in practice. Could @brentschmaltz or someone else elaborate a bit? Obviously I need to do something because signature validation fails, but what? I'm using Python and would just like to validate that user didn't forge the JWT token.

metaclassing commented 6 years ago

I feel like between the various implementations for AAD floating around using access_token there needs to be some much improved communication from Microsoft.

I stumbled across this thread by accident after spending an hour trying to figure out why I was unable to validate a token independently (sending every token to the graph API for verification is not the fastest thing in the world to be doing).

The whole "special processing" phrase has popped up across several discussions now and I am concerned about the opaque nature of this implementation especially around something as important as authentication which MUST be open and well documented to be considered trustworthy and free from security defects.

btastic commented 6 years ago

Could please someone provide a way to do the "special processing"?

somdey commented 6 years ago

Same here I too have "nonce" in my token. Could please someone talk about 'special processing' ?

brentschmaltz commented 6 years ago

@smndey @btastic @metaclassing @lokori I've been OOF most of the past month. Back now. I need to understand how you obtain that token and why you want to accept it. If the audience is 'microsoft.Graph' you shouldn't be accepting it. Only tokens that are for the host, should be accepted.

The 'nonce' is a mechanism, that allows the receiver to determine the token was forwarded. The signature is over the transformed nonce, so if you try and validate it directly, the signature will fail. Graph can determine that this token was replayed.

metaclassing commented 6 years ago

Good afternoon @brentschmaltz

I suspect there are a variety of use cases and desires in the greater community of developers who want to consume identity, especially in an enterprise setting.

From a purely academic perspective, I do not believe secrecy results in better security. If Microsoft plans to thrive in a open web service landscape it needs to provide significantly more transparency in token generation, token validation, as well as changes to the AAD API (There have been a couple examples lately where un-communicated changes resulted in BC breaks. Software that was working for months mysteriously stopped with token-can-not-be-generated errors)

From a performance perspective in AAD 1 applications that do not have a NONCE I would not want to have to check with the graph API every time a user presents me a token to validate it is good, and have taken to using a shared redis cache between calls to keep from having to do that.

The most applicable example I have though is using MSAL with front-end web applications that call to multiple back-end APIs. The front end needs to authenticate the user and use their identity token to request a graph API scoped access token to call the magic /me endpoint for displaying basic user information.

In the current model the front end SPA also needs to use the same identity token to request access tokens for 3 back-end API services that are not the graph API. Currently the challenge is on initial authentication this model would send the user to AAD for authorization 3 times for 3 different apps with scopes as each back end API has its own client id and secret.

This authorization flow is not ideal (or acceptable as the number of back-end APIs grows) and requires a solution such as the use of a dedicated front-end API or implement an API gateway to scale the number of back-ends doing common authentication and authorization at which point the value add of AAD is somewhat diminished.

One feature people have wanted for years is a variable approach to security for consuming identity services, such as a multi-audience access token. The back end API's generally only consume the access token to authenticate the user and perform their own authorization. My web services tend to call the graph API using their own client id and secret to get an access token and retrieve user group information et al eliminating much of the user authorization (or worse, admin authorization) required.

I am aware that this approach is considered "less secure" because API 1 could use the same graph token to authenticate to API 2 as the user, but that may be a known and acceptable risk for some applications enabling a much smoother authentication flow and avoiding the need to juggle tokens in the UI.

Ultimately if I decide to trust a token generated for an audience other than my client id, that is my call to make. Similarly if I decide that I want to trust the token for longer than AAD is willing to issue it for (days rather than hours) then I control that logic as well. Microsoft appears to be using opaque token validation logic to prevent me from controlling the security / convenience trade-off with my software.

V1122am commented 6 years ago

Could please someone provide a way to do the "special processing"?

jvandervelden commented 6 years ago

We learned that validating the Graph token is not required. The graph token you get from either doing a web flow or api impersonation will be validated when you make the actual Graph api call. We don't care if it's valid, that will be shown in time when we go do the request and get a 401/403 back.

You don't want to use the Graph token as your main app's token, you will want to create an app registration in the portal, which these tokens you can validate.

For multiple api endpoints, that's an architecture decision you make. You can go with an api gateway and have all your services/endpoints behind 1, this allows you to only have 1 token for multiple services. Or you can decide to have each api it's own "application" and require a separate token for each. If you go with the separate tokens you can just follow the impersonation flow and use the main app token to get the sub endpoint tokens and the experience is seamless to the user. In the current web any given app needs to keep track of multiple tokens anyway since it will hit multiple resources not necessarily on the same domain or even the same OAuth authority.

If you are seeing an 'nonce' in your main app's token, I've seen this when doing the redirect to login.microsoft.com and having '?nonce=' in the url parameters.

brentschmaltz commented 6 years ago

@metaclassing Thanks for your feedback.

Let me propose this perspective as I don’t believe there is an attempt to hide anything here.

Note: this ‘nonce’ is different than the ‘nonce’ in the JWT Payload.

MsGraph recognized an opportunity to improve security for users. They achieved this by putting a ‘nonce’ into the jwt header. The JWS is signed with a SHA2 of the nonce, the ‘nonce’ is replaced before the JWS is serialized. To Validate this token, the ‘nonce’ will need to be replace with the SHA2 of the ‘nonce’ in the header. Now this can change since there is no public contract.

We do recognize the need for customization. This IdentityModel library has multiple extensibility points for validation: Audience, Issuer, Lifetime, Signature, SigningKey, TokenReplay. A delegate is settable on TokenValidationParameters. If there is something you need, please let us know, we are very open to suggestions.

I can’t speak to AAD API breakage, but I can speak to Identity and Access where multiple down-level services are accessed for a single call. As you pointed out, accepting a token for a different audience opens a forwarding attack. There is a model in the works that addresses this issue: https://tools.ietf.org/html/draft-ietf-oauth-signed-http-request-03

App1 obtains a token for App2 that represents a trust between them. When App1 requests a token for App2, a public key is sent with the request to the IdentityProvider. When App1 calls App2, the call is essentially signed. The original token that represents the caller can be included. This model provides the identity of the original caller, protects against leaking of tokens and provides high confidence to App2 that it was App1 that called. The token presented to App2 has the audience for App2. The identity token will have the audience of App1. Custom validation can scope a set of audiences on the identity token.

zbrad commented 5 years ago

At the end of the day, Msft (Azure AD) is currently generating JWT Tokens that fail signature validation.

Therefore Msft is non-IETF compliant by failing to generate valid JWT Tokens, eg tokens which can be validated by their provided signature.

So, when will Msft fix this?

brentschmaltz commented 5 years ago

@zbrad it is not completely accurate to say that tokens generated by AzureAD are not compliant.

There is no implementation defined in oAuth as to what constitutes an access_token for a ResourceServer. It is a contract between the IdentityProvider and the ResourceServer. Since this token is meant for graph and graph can validate it, it is all perfectly fine. It's questionable to make access decisions based off a token that wasn't issue to the receiver.

https://tools.ietf.org/html/rfc6749#section-1.4

homutov commented 5 years ago

To Validate this token, the ‘nonce’ will need to be replace with the SHA2 of the ‘nonce’ in the header.

@brentschmaltz I've tried to replace the nonce in the header, but still was not able to validate the access token signature. Could you, please, be more specific about the details of that replacement (e.g. SHA2 = SHA256 with Hex string representation or what)?

brentschmaltz commented 5 years ago

@homutov sure

            string hashAlgorithm = null;
            try
            {
                var alg = header.Property(JwtHeaderParameterNames.Alg).Value.Value<string>();
                if (!TokenValidator.HashAlgorithmMap.TryGetValue(alg, out hashAlgorithm))
                    hashAlgorithm = SecurityAlgorithms.Sha256;

                nonceProperty.Value = GetHashedNonce(hashAlgorithm, nonceProperty.Value.Value<string>());
            }
            catch(Exception ex)
            {
                throw new TokenCreationException(LogHelper.FormatInvariant(LogMessages.S2S32205, ex));
            }
    private static string GetHashedNonce(string algorithm, string nonce)
    {
        HashAlgorithm hashAlgorithm = null;
        try
        {
            if (string.IsNullOrEmpty(algorithm))
            {
                hashAlgorithm = SHA256.Create();
            }
            else
            {
                switch (algorithm)
                {
                    case SecurityAlgorithms.Sha256:
                        hashAlgorithm = SHA256.Create();
                        break;

                    case SecurityAlgorithms.Sha384:
                        hashAlgorithm = SHA384.Create();
                        break;

                    case SecurityAlgorithms.Sha512:
                        hashAlgorithm = SHA512.Create();
                        break;

                    default:
                        throw new TokenCreationException("hash algorithm not supported");
                }
            }
            return Base64UrlEncoder.Encode(hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(nonce)));
        }
        finally
        {
            if (hashAlgorithm != null)
                hashAlgorithm.Dispose();
        }
    }
KhaledSMQ commented 5 years ago

this issue let me spend 3 days trying to validate graph access token,

brentschmaltz commented 5 years ago

@KhaledSMQ I am sorry you lost those 3 days. Was this due to the 'nonce' in the header? Validating a token for Graph?

KhaledSMQ commented 5 years ago

@brentschmaltz Yes i was trying to validate Graph access_token, according to Microsoft documentation i must validate both access_token and Id token, https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens#validating-tokens

https://docs.microsoft.com/en-us/azure/active-directory/develop/id-tokens#validating-an-idtoken

https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-tokens#access-tokens

When your API receives an access token, it must validate the signature to prove that the token is authentic. Your API must also validate a few claims in the token to prove that it is valid. Depending on the scenario requirements, the claims validated by an app can vary, but your app must perform some common claim validations in every scenario.

Any way after read all the thread I understand that i should not use the access_token of ms graph for other than consuming graph api's. basically i switch to use id token to make sure that the authenticated user belong to my organisation and he is authenticated. Please correct me if this i am doing wrong.

brentschmaltz commented 5 years ago

@KhaledSMQ you are correct. An access_token is required by a resource server to ensure the caller has rights to specific resources. There is no consistent definition of the structure of an access_token. In this case an access_token obtained for graph, will be validated by graph. The id_token will identify the user and organisation they belong to.

The id_token that arrives with an access_token is not required to be signed. This is unfortunate as it has led to a lot of discussion, but reduces cpu usage.

laurentbel commented 5 years ago

I have the same error and it was due to I have setting into scope field of token request (doc) the graph app uri: https://graph.microsoft.com/.default.

I have change it for my App ID URI adding /.default and it works.

Note that this works well BUT just because the access token is actually an id token. This is specified in this official documentation : https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope

This produces a consent screen for all registered permissions (if applicable based on the above descriptions of consent and /.default), then returns an id_token, rather than an access token. This behavior exists for certain legacy clients moving from ADAL to MSAL, and should not be used by new clients targeting the Microsoft identity platform endpoint.

So be careful because you will be manipulating an id token instead of an access token. And it meant to be used for certain legacy client only... You are warned !

cforycki commented 5 years ago

I'm experiencing the same issue. We use Azure AD to authenticate users, but our APIs are still on our servers, we are not using any Microsoft servers for our APIs.

If I trust the documentation from Microsoft :

id_token | A signed JSON Web Token (JWT). The app can decode the segments of this token to request information about the user who signed in. The app can cache the values and display them, but it shouldn't rely on them for any authorization or security boundaries. For more information about id_tokens, see the id_token reference.

So I shouldn't use the id_token for security, but I can't verify the access_token because Microsoft do some stuff to the access_token and their own certificate doesn't validate their own tokens.

Tried what been said on the nonce in header. Tried to extract it, change its value to SHA256, and reconstruct the JWT. Unsuccessfully, the JWT was still not valid. And this can't be a valid solution. If tomorrow Microsoft decide to change this and use another algorithm, everything breaks.

Microsoft should provide tokens that can be validated with provided public key.

What can I do for now ? Using id_token even if it's explicitly said I should not ?

jmprieur commented 5 years ago

@cforycki : is the access token for your own Web API? and are you trying to validate it in your Web API? or is it for another API?

cforycki commented 5 years ago

@jmprieur Yes it's for our own Web API. We have a JS frontend which use msal.js, and then give the token in Authorization header as Bearer to the backend which validate the token on each request. Kind of standard with JWT these days.

User rights are managed on our end, so I don't know if we really need the access_token, and if the id_token could be enough (even though it's stated that we should not). Or maybe we are doing something wrong and should change our way to use Azure AD.

jmprieur commented 5 years ago

I you are validating the token for your Web API in the Web API this should work (what you cannot/should not do is validate in an app a token which is not for the app).

Didn't you find useful information in https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-protected-web-api-overview ?

cforycki commented 5 years ago

@jmprieur The token is indeed validated in our API backend, and actually id_token is correctly validated with the public key provided by microsoft. If it's the way to go, then there is no problem, but either the docs are confusing, or I misunderstand something (English is not my native language).

In the link you are providing, they also use access_token as the bearer. And even on the first page, if you go to the "ID tokens" link in prerequisites, it's stated :

ID Tokens should be used to validate that a user is who they claim to be and get additional useful information about them - it shouldn't be used for authorization in place of an access token. The claims it provides can be used for UX inside your application, keying a database, and providing access to the client application.

I indeed use it to provide access to the client application (the one in JS, the frontend), but it's stated it should not be used for authorization in place of an access_token. With that information, I obviously assumed I should send the access_token to our Web API. But unfortunately I have been unable to validate these access_token.

jmprieur commented 5 years ago

@cforycki : there is one case where you can pass the ID token to your backend: when the frontend and the backend are the same app (have the same ApplicationID / ClientID)

I'm surprized you cannot validate your access token. Did you try our samples: https://github.com/Azure-samples/active-directory-dotnet-native-aspnetcore-v2/ (the client is a WPF app, but you can easily replace it by a JS app)

cforycki commented 5 years ago

@jmprieur They are the same app, they will be deployed on the same application server, under the same context. and the Web API will be under /api. But it's still only one app, and registered as one app. So I'll use id_token as for now.

I didn't try those samples. I should have stated our backend is in Java. So based on what brentschmaltz said about the nonce in header, tried to do something similar in Java. I used jjwt library to validate the token. The id_token is correctly validated if I don't do anything to it, so I certainly made a mistake when trying to change the nonce in the access_token header. If you have any sample in Java I would be glad to have a look, if not I will try to provide some code I used.

EDIT: Here is a portion of the code I used. Not perfect, not meant to be production-ready of course.

public String getTokenWithHeaderNonceReplaceBySHA2(String rawToken) {
   if (StringUtils.isBlank(rawToken)) {
       return null;
   }

   String[] tokenParts = rawToken.split("\\.");
   if (tokenParts.length != 3) {
      return null;
   }

   String header = getStringFromBase64(tokenParts[0]);

   Pattern p = Pattern.compile("\"nonce\":\"(?<nonceValue>.*?)\"");
   Matcher m = p.matcher(header);
   String nonce;
   if (m.find()) {
        nonce = m.group("nonceValue");
    } else {
        return null;
    }

    byte[] nonceSha2Bytes = DigestUtils.sha256(nonce);
    String nonceSha2 = new String(Base64.getEncoder()
                                        .encode(nonceSha2Bytes));

    String newHeader = header.replaceAll("\"nonce\":\".*?\"", "\"nonce\":\"" + nonceSha2 + "\"");
    String newHeaderBase64 = Base64.getEncoder()                                     
                                   .encodeToString(newHeader.getBytes());
    String[] tokens = {newHeaderBase64, tokenParts[1], tokenParts[2]};
    return StringUtils.join(tokens, ".");
}

As it's not working, it's surely wrong. It produces a correct JWT but still can't be validated.

By the way, even on https://jwt.io , the access_token cannot be validated if I copy paste (between the begin and end tags) the x5c value I got from https://login.microsoftonline.com/common/discovery/keys

fabien-h commented 5 years ago

I don't understand why msal.js cannot make a request without the nonce.

Is there no way to configurate the application in the azure portal to answer without it ? Or to pass parametters to acquireTokenSilent to avoid the nonce ?

fabien-h commented 5 years ago

I have the same error and it was due to I have setting into scope field of token request (doc) the graph app uri: https://graph.microsoft.com/.default.

I have change it for my App ID URI adding /.default and it works.

I'm not sure to understand what you did. Could you elaborate ?

vzhikness commented 5 years ago

Hi all,

My conclusion about this topic:

  1. on our backend we can't validate access_token from v2.0 endpoint because of nonce claim in the header of JWT
  2. on our backend we can validate access_token from v1.0 endpoint
  3. on our backend we can validate id_token from both v1.0 and v2.0 endpoint

We chose second variant because we want to use OAuth 2.0 client credentials flow and we want to validate it on our backend. This flow gives you only access_token.

Useful link: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/815

Thanks

cforycki commented 5 years ago

Hi,

We finally succeeded with the token validation. In fact, it's was only a configuration problem.

On the frontend application, we used MSAL.js. Everything was fine except the scopes we requested for login and acquiring tokens.

On the Azure Portal, if you go to your application page, on the left there is a "Scopes" menu entry. There was one scope defined for our application (if not, I think you can add it. I'm not he one in charge of the configuration here, so can't check).

This value need to be used in MSAL.js as your scope.

If you use scopes like "User.read", "openid", "profile", "email", ... You'll see that the audience value in the JWT access token will be set to graph.microsoft.com. So the token is indeed for graph, and not for your application.

If you use the scope you have declared in your application on the portal, you'll see that the audience is the same value as the scope and the token will be valid ! Our scope looks something like this : https://\.onmicrosoft.com/\/user_impersonation

We had the help of a Developper from Microsoft for this solution :)

felixwa commented 5 years ago

Set scope to something like api://{guid}/{scope_name} then the audience will be your application then the token could be successfully validated.

DinoChiesa commented 5 years ago

@cforycki - your effort is doomed. I will explain why.

First, some background.

@zbrad stated (https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/609#issuecomment-430741177) that the MSFT endpoint is generating a thing that looks like a JWT, and has a header that has all the same fields as a JWT, but is not in fact a JWT, because it cannot be validated according to the IETF spec for JWT. Hard to argue with this logic.

@brentschmaltz responded (https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/609#issuecomment-430835714) by saying this thing-that-looks-like-a-JWT is just an access token, and there's no requirement that it be a JWT. He is also correct. What he is not saying is this: "I agree that making this thing that is not a JWT look very much like a JWT causes confusion."

People asked, "what is the special processing?" (required to validate such a token) On November 9 2018 (https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/609#issuecomment-437471108) Brent Schmaltz didn't answer directly, but showed the code that performs the token GENERATION. You can infer that it is for generation by the TokenCreationException in the code snip. It shows that the nonce in the header is swapped with a SHA256 of the actual nonce.

He doesn't say what happens before and after that, but what I suppose is: BEFORE that step, the signature has been created on the header with the actual nonce (which can be any string) along with the secure payload. Concatenating the base64url encoded version of {header, payload, signature} would result in a JWT, but that's not what gets sent out. First, the surgery happens in which the header's nonce value gets replaced with the sHA256 of the nonce. Then the assembly of dot-concat(base64url(header), base64url(payload),base64url(sig)). This results in a thing that looks very very much like a JWT but is not, because the signature cannot be verified.

Your code shows accepting that thing and computing the sha256 of the asserted nonce in the header. But keep in mind, this "nonce" in the header is already the sha256 of the actual nonce. Performing a sha256 on the sha256 of the nonce does not get you the original nonce. In fact the sha256 is a MAC, a one-way encoding. There is no way to get the original nonce, if you start with the sha256. This is why your attempts to verify the signature are doomed to fail.

There is no way to validate the signature on this thing-that-looks-like-JWT, unless you have the original nonce. If you have THAT, then you can swap out the sha256-of-nonce with the actual nonce, and then the signature will verify. It's kind of like the inverse of what you have in your Java code.

The solution is to persuade AzureAD to NOT issue a token that looks very much like a JWT but is not a JWT. And the way to do that, is to specify a scope that is anything other than https://graph.microsoft.com/.default. When you use that thing as the scope, then AzureAD issues a token that is not a JWT, and is intended for consumption only by the Graph API. When you use a scope that is something other than that, then AzureAD issues a token that is a JWT and can be validated by any party that has access to the public key (via JWKS).

xsreality commented 5 years ago

I recently had to setup Azure AD authentication with OIDC and came across all the problems mentioned in this thread and some more. I documented everything here. Hope it helps anyone looking to make this work. https://medium.com/@abhinavsonkar/making-azure-ad-oidc-compliant-5734b70c43ff?source=friends_link&sk=45dc380108a3ad521b2bde0cfed28130

hpsin commented 5 years ago

Hey everyone, owner of OAuth tokens at Microsoft here, with a helpful message.

Stop. Stop everything in this thread. Access tokens are opaque blobs of text that are for the resource only. If you're a client getting a token for Graph, assume that it's an encrypted string that you should never look at - sometimes it will be. We use a special token format for Graph that they know how to validate - you shouldn't be looking at access tokens if they're not for you.

From the original documentation:

When your API receives an access token,

This means "When your API is sent an access token by a client" - meaning it's a token that they actually requested from AAD to send your API. If they're sending you Graph tokens, you're at immediate risk of being broken and will break in horrible and insecure ways at some random time in the future.

Again: Stop looking at access tokens if they're not for you to look at. Easy way to know? The aud claim will match something that you own if they're for you. If it doesn't match, never look at it.

ID tokens are signed on v2, and can be validated. If you want to auth with a back-end then you can use an id_token if you know what you're doing - but don't use it to auth against other servers or services. Use access tokens instead. Access tokens that are actually requested and consented to for that actual API. Never use another app's tokens to do authentication, you will break.

xsreality above makes the correct point in their blog post - if you want to validate an access token to communicate between your front and backend, you should create your own scope for the backend, preauthorize it, and then get a token to call it, thus guaranteeing that the token is actually issued for your app. I would request that you rephrase it as a "problem" though - the problem is you broke the cardinal rule of authentication by using someone else's token in your own auth flow :)

xsreality commented 5 years ago

@hpsin Hi Hirsch, thanks for your comment. The reason I call it a "problem" is because by following the documentation and going by the OpenID spec, Azure AD does not behave as an Open ID provider. It supports it but with tweaks that must be done which I have documented in the blog post above.

Let me give you an example. The OpenID spec says a user info endpoint must be provided secured via access token. The v2.0 OpenID metadata configuration endpoint for an Azure AD tenant returns the userinfo_endpoint as https://graph.microsoft.com/oidc/userinfo. When access token is requested for my own backend using custom scope (not Graph API), I cannot call the user info endpoint as the token is rejected effectively breaking OpenID compliancy. What is the Azure solution?

hpsin commented 5 years ago

Access tokens are resource specific - an access token for your resource cannot be accepted by the UserInfo endpoint. But if you request id_token+token for scope=openid+profile+email, you'll get back an access token for the Userinfo endpoint.

See the OAuth2 spec: The resource server MUST validate the access token and ensure that it has not expired and that its scope covers the requested resource.

mahoekst commented 5 years ago

Hey everyone, owner of OAuth tokens at Microsoft here, with a helpful message.

Stop. Stop everything in this thread. Access tokens are opaque blobs of text that are for the resource only. If you're a client getting a token for Graph, assume that it's an encrypted string that you should never look at - sometimes it will be. We use a special token format for Graph that they know how to validate - you shouldn't be looking at access tokens if they're not for you.

From the original documentation:

When your API receives an access token,

This means "When your API is sent an access token by a client" - meaning it's a token that they actually requested from AAD to send your API. If they're sending you Graph tokens, you're at immediate risk of being broken and will break in horrible and insecure ways at some random time in the future.

Again: Stop looking at access tokens if they're not for you to look at. Easy way to know? The aud claim will match something that you own if they're for you. If it doesn't match, never look at it.

ID tokens are signed on v2, and can be validated. If you want to auth with a back-end then you can use an id_token if you know what you're doing - but don't use it to auth against other servers or services. Use access tokens instead. Access tokens that are actually requested and consented to for that actual API. Never use another app's tokens to do authentication, you will break.

xsreality above makes the correct point in their blog post - if you want to validate an access token to communicate between your front and backend, you should create your own scope for the backend, preauthorize it, and then get a token to call it, thus guaranteeing that the token is actually issued for your app. I would request that you rephrase it as a "problem" though - the problem is you broke the cardinal rule of authentication by using someone else's token in your own auth flow :)

this! I always explain as it being a private contract between the resource and the IDP. You cannot know the details. It might be an encrypted token tomorrow. Blue might mean yellow, or everything is bitshifted 1 to the left. The resource and the IDP (AAD) have that understanding. You, at the moment, be able to read that AT since it's a JWT is just a coincidence and you should tread the token as a binary blob you cannot read if you are not the resource.