Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support On-Behalf-Of flow #53

Closed
rayluo opened this issue Jun 5, 2019 · 21 comments
Closed

Support On-Behalf-Of flow #53

rayluo opened this issue Jun 5, 2019 · 21 comments

Comments

@rayluo
Copy link
Collaborator

rayluo commented Jun 5, 2019

The following description is derived from a description, written by @psignoret:


To illustrate how this works, let's suppose you have the following:

  1. A client application that the end-user interacts with: ClientApp.
  2. A backend API used by your client app(s): BackendAPI (which has no user interface whatsoever, but is configured to use Azure AD for authentication).
  3. A third-party API that you have no control over, that requires user authentication via Azure AD: DynamicsAPI (which also has no user interface whatsoever).

With the on-behalf-of flow, the following would happen:

  1. The user signs in to ClientApp, by using any of the flows that include the user using the normal browser-based login flows. If the user needs to do MFA, provide consent, change their password, or any other sign-in interruption, they can do that. At the end of the sign-in, ClientApp will have an access tokenin the name of the authenticated user, destined for BackendAPI.
  2. ClientApp will present this access token to BackendAPI (e.g. in the Authorization HTTP header).
  3. BackendAPI will validate this token (e.g. check that its signature is good, that the claims are valid, etc.).
  4. Now, BackendAPI can't call DynamicsAPI and provide the same access token it was given (since it is destined for BackendAPI, and DynamicsAPI will reject it). Instead, BackendAPI needs to get a new access token, on behalf of the signed in user. To do this, BackendAPI makes an authenticated token request (meaning BackendAPI authenticates itself with a client secret or certificate) to Azure AD. This request includes:
    • An indication that the resulting access token should be for DynamicsAPI.
    • An indication that this is an "on-behalf-of" (OBO) request.
    • The original access token that BackendAPI received (remember, to get this token, ClientApp put the user through a sign-in, so it includes all the details about the user who signed in).
  5. If everything checks out, Azure AD will respond with an access token for DynamicsAPI, on behalf of the signed in user. BackendAPI can now call DynamicsAPI with that token.

Step 4, above, will be very easily invoked with MSAL Python:

  • app.acquire_token_on_behalf_of(user_assertion, scopes)

In the signature above, scopes would be the scope(s) of DynamicsAPI (e.g. ["https://contoso.crm.dynamics.com/.default"]), app is a confidential client representing BackendAPI, and user_assertion is what contains the access token that ClientApp originally obtained for BackendAPI.

Here's a quick diagram illustrating the token and API requests happening:

image

@rayluo rayluo added this to the MSAL Python 0.6.0 milestone Jun 17, 2019
@dudil
Copy link

dudil commented Jul 14, 2019

Very much required. Anything to assist you with that?

@rayluo
Copy link
Collaborator Author

rayluo commented Jul 15, 2019

@dudil Thanks for reaching out! We appreciate assistant from community on:

  • Upvoting features by +1 so that we take that sorted list of issues into consideration in our planning. You just did that. :-)
  • As an open source project, contributions on implementations are always welcome, and then we will join forces to do code reviews. In this particular case, since this feature is already at a high spot in our list, and tentatively scheduled for upcoming release, we are on it.
  • Per our work flow, we always cross-reference issues with their actual PRs and/or release. So please subscribe this issue so that you will receive github notifications, and then please help us test it in your project.
  • Now the exciting part. Spread the love by building your existing/next great project on top of our library and make the world better. (Oh @dudil you are already on it. Great!)

@dsanghan
Copy link

@rayluo Is there any progress on this? Seems like 0.6.0 is scheduled for July 22nd - is that still on track?

@dudil
Copy link

dudil commented Jul 18, 2019

@dudil Thanks for reaching out! We appreciate assistant from community on:

  • Upvoting features by +1 so that we take that sorted list of issues into consideration in our planning. You just did that. :-)
  • As an open source project, contributions on implementations are always welcome, and then we will join forces to do code reviews. In this particular case, since this feature is already at a high spot in our list, and tentatively scheduled for upcoming release, we are on it.
  • Per our work flow, we always cross-reference issues with their actual PRs and/or release. So please subscribe this issue so that you will receive github notifications, and then please help us test it in your project.
  • Now the exciting part. Spread the love by building your existing/next great project on top of our library and make the world better. (Oh @dudil you are already on it. Great!)

Already implemented some of the flows (need to update my read-me...)
I'll be happy to assist with validating and testing that when avilable.

Thanks!

@dsanghan
Copy link

@dudil: I don't see the code for on-behalf-of flow in: https://github.com/dudil/py365/blob/master/py365/auth/msal_connection.py - nor is there a different branch on that repo.

Nor do I see a fork you've made of this library. Where are the flows implemented?

@dudil
Copy link

dudil commented Jul 19, 2019

Hi @dsanghan - I didn't implement that since it is not working yet.
I've just pushed some more flow - look at the same file now.
Public wise: It has Usr + PW and device token
Confidential: app + secret and hoping to have the on behalf of once implemented.

If you wish me to fork and implemented I can try but as I understood you already developed that - did you?

@dsanghan
Copy link

@dudil: No don't need any of that. I thought you had implemented

but ya, we'll have to wait until that is implemented.

@rayluo Is there any progress on this? Seems like 0.6.0 is scheduled for July 22nd - is that still on track?

@rayluo
Copy link
Collaborator Author

rayluo commented Jul 20, 2019

Hi folks, thanks for your interest and sorry for some confusions here.

  • Just recently we began to use github milestones to organize our work flow. This particular On-Behalf-Of (OBO) feature was originally scheduled in milestone 0.6.0 but then we moved it to another milestone. And it turns out that event line here added by github does NOT make it clear that this feature is no longer part of the milestone 0.6.0, thus causing @dsanghan 's confusion. Sorry about that.
  • Milestone 0.6.0 originally contained TWO major features, the "supporting ADFS 2019 on premesis" and the OBO. The ADFS 2019 feature made good progress recently, to a point that we feel there is no point to block our ADFS 2019 customers by forcing them to wait for the irrelevant OBO feature. So we narrowed the milestone 0.6.0 scope and will ship ADFS 2019 earlier than its original ADFS+OBO release date.
  • This does not mean OBO development has been delayed. It will be finished with roughly the original pace.
  • Lastly, regarding to the ETA of a milestone. I used to work for another big tech firm who never misses an ETA. How? Well, they never publish an ETA beforehand. :-) Here in this repo we are doing this experiment to provide ETA for transparency. By the law of probability, we won't 100% make it. Later we will revisit this practice and see whether it brings transparency or just confusion, and we will go from there.

UPDATE at 2019-7-22: That ADFS 2019 PR is technically ready to go, but we hold off for a little longer to conduct some internal review/discussion. (PS: We don't normally provide progress update on a delayed release, but based on the conversations above, we figure it might be helpful for audience here.)

@rayluo
Copy link
Collaborator Author

rayluo commented Sep 12, 2019

Hi @dudil , remember you mentioned this?

I'll be happy to assist with validating and testing that when avilable.

Thanks!

Now the time has come. :-) Please try #92.

CC: @dsanghan @sohels @arindam-laha who also expressed interest in this feature.

@dsanghan
Copy link

dsanghan commented Sep 20, 2019

@rayluo Not working for us.

{'error': 'invalid_grant', 'error_description': "AADSTS50013: Assertion failed signature validation. [Reason - The provided signature value did not match the expected signature value., Thumbprint of key used by client: '89EFEA5825E15F1B75CC812CBB873B69C4151A7E', Found key 'Start=07/15/2019 00:00:00, End=07/15/2021 00:00:00']\r\nTrace ID: 278b62a5-c279-4b7c-a2ae-72d988890f00\r\nCorrelation ID: 270da176-d294-43fe-8cf9-645c86b1644f\r\nTimestamp: 2019-09-20 06:55:05Z", 'error_codes': [50013], 'timestamp': '2019-09-20 06:55:05Z', 'trace_id': '278b62a5-c279-4b7c-a2ae-72d988890f00', 'correlation_id': '270da176-d294-43fe-8cf9-645c86b1644f'}

The JWT access token was created using https://github.com/AzureAD/microsoft-authentication-library-for-objc on the v2 endpoint with scope: https://outlook.office.com/EWS.AccessAsUser.All

The issued JWT token from the objc library works for our requests, and refreshes on the V2 endpoint without issue.

Code to reproduce:

app = ConfidentialClientApplication('<Client ID>', client_credential='<Client Secret>')
app.acquire_token_on_behalf_of('<Access Token>', ['https://outlook.office.com/EWS.AccessAsUser.All'])

I verified tenant and token_endpoint on the ConfidentialClientApplication:

>>> app.authority.token_endpoint
'https://login.microsoftonline.com/common/oauth2/v2.0/token'

Any ideas?

@dsanghan
Copy link

Here's the _data being passed to the post request inside oauth2.Client:

Screen Shot 2019-09-20 at 12 57 35 PM

The assertion (JWT token) decodes properly on JWT.io as well.

@rayluo
Copy link
Collaborator Author

rayluo commented Sep 20, 2019

@dsanghan Glad to have you onboard for testing. :-)

You mentioned that your access token was obtained by MSAL for Objc, with scope https://outlook.office.com/EWS.AccessAsUser.All. So that is a token for your mobile app to access EWS. You can double check its audience aud in http://jwt.ms, to see it differs from your confidential app's client_id. Such a token does not belong to your confidential app.

OBO is typically used by your confidential app, which is running on your backend server, between your front-end app and the downstream service. Please refer to the description in this issue.

@dsanghan
Copy link

@rayluo The aud claim, and the app id are indeed different.

I'm trying to use OBO since it matches the use case you described. The front-end app is where a user logs in. Since on iOS, you cannot run indefinite network connections, we have a confidential app on a backend server that checks for new emails OBO the user using EWS, if the user opts in for it. IF there is a new email, it sends a notification to the front-end app. AFAIK, this should be a valid OBO use case.

If I can't use OBO for this, what do you suggest? Sending the refresh / access tokens from MSAL objc straight to my backend server for usage?

@dudil
Copy link

dudil commented Sep 21, 2019

Hi - Sorry to jump in the middle of it, I was busy in the last few days so was kind of out of the loop.
@dsanghan - From your scenario, I don't believe OBO is the right solution for you.
Let me try and explain what the two different scenarios you may use are:

  1. Public Accesses Token - this is done when your application is "public."
    (To be clear - application means both client-side and backend.
    it does not mean application in the term of mobile app)

  2. Confidential Access Token - this is done when you application is being installed via the administrator of the tenant and hence it is a B2B app. It is then used to manage administrative tasks on the tenant. Think about it as a cron job for the tenant that uses the MS Graph.

For public access token, you either need a specific user consent via the permission page.

For the confidential access token, you will need to have an app secret from the AAD configuration page.
So to make it very easy to distinguish between them Confidential Access Token must have an app secret when Public Access Token doesn't have it.

Without going into too many picky details -
Every API documentation states if it is supporting both models.
Delegated - is usually Public while Application - is usually Confidential. (Again, not related to actual application it is just the usage and not the architecture of your service).
In the case of the Messages API, luckily all are supported so you can use either one.
https://docs.microsoft.com/en-us/graph/api/user-list-messages?view=graph-rest-1.0&tabs=http

So when do you need to use the OBO? In my case, I have an administrative service but the API I'm trying to use is not supporting the application permission type (the Planner API)
Hence, the only way I can create an administrative service (like cron job) is either store the username and password (which is wrong in so many ways...) or use the OBO flow.
https://docs.microsoft.com/en-us/graph/api/plannerplan-get?view=graph-rest-1.0&tabs=http

I hope this makes more sense now...

@rayluo - I will try the changes hopefully today/tomorrow and update.

@rayluo
Copy link
Collaborator Author

rayluo commented Sep 21, 2019

Quick comment to @dsanghan : In your scenario, sending access token (AT) and refresh token (RT) to your backend, which uses same client_id as your mobile app, can be a valid option. In fact, that is described as an "other strategies present themselves" alternative in the On-Behalf-Of doc in its "use of a single application" section.

@dsanghan
Copy link

@rayluo Ok I'll try it out. What's interesting is that MSAL objc seem to be discouraging this use case: AzureAD/microsoft-authentication-library-for-objc#618 - they have purposefully not given access to the refresh token. While it's not hard to add the relevant code to get it, I wonder why there is a discrepancy here?

@dsanghan
Copy link

@rayluo I was able to use the same refresh token on the backend to get / update access tokens, so seems like everything is working. Still curious why MSAL objc recommended OBO in the above linked issue.

@jmprieur
Copy link

@dsanghan : I believe that you shoud not pass the refresh token to your backend (just the access token). Once the refresh token is used, it may not be usable again, and should be immediately replaced by the new RT you obtained from OBO

@dsanghan
Copy link

dsanghan commented Sep 23, 2019

@jmprieur As @rayluo pointed out, OBO won't work in our case. When I try to get a new pair of access/refresh tokens via OBO, the user assertion is rejected with a signature verification failure since the audience value is different from the client ID (since the JWT token is for EWS, acquired using MSAL objc). Ideally I'd use OBO but doesn't seem to work. More details as described above.

@jmprieur
Copy link

@dsanghan : would you want to apply a technics like presented in 3.-Web-api-call-Microsoft-same-client-id in the ASP.NET Core Web API tutorial (I know it's not Python, but the principle would still apply)

@rayluo rayluo mentioned this issue Sep 25, 2019
@rayluo
Copy link
Collaborator Author

rayluo commented Nov 12, 2019

Released in version 0.7.0

@rayluo rayluo closed this as completed Nov 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants