Azure / azure-sdk-for-go

This repository is for active development of the Azure SDK for Go. For consumers of the SDK we recommend visiting our public developer docs at:
https://docs.microsoft.com/azure/developer/go/
MIT License
1.64k stars 842 forks source link

Support Oauth Code Flow #22818

Closed denen99 closed 4 months ago

denen99 commented 6 months ago

Feature Request

chlowell commented 6 months ago

Can you please add some more details about what you're trying to do and the change you would like to see?

github-actions[bot] commented 6 months ago

Hi @denen99. Thank you for opening this issue and giving us the opportunity to assist. To help our team better understand your issue and the details of your scenario please provide a response to the question asked above or the information requested above. This will help us more accurately address your issue.

denen99 commented 6 months ago

Hi Charles -

I have an Azure App I am building that will be used for clients of various MS Tenants to Oauth and give permission to in order for us to scan their onedrive in the background. To do this, we are using the auth code flow ( https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow), the user gives consent for the delegated permissions (File.Read, File.Read.All and offline_access), we get a code and we exchange that code for an access and refresh token.

Now on our backend, we want to instantiate an Azure SDK Client for our application using azidentity, but my understanding is this flow is not supported in the Azure SDK for Go. azidentity.NewClientSecretCredential seems to not be intended for delegated permissions, only app permissions and my understanding is creating a client with this flow is not supported.

Once we have a credential object, we want to pass that to the msgraphsdk Go library so that we can query the various users OneDrive folder contents.

Does that make sense? I could be misunderstanding but none of the existing New[FOO]Credential methods seemed to support this flow, perhaps I am missing something. I tried DeviceCode and ClientSecret and neither of them worked without errors.

Thank you ! Adam

On Wed, May 1, 2024 at 6:32 PM Charles Lowell @.***> wrote:

Can you please add some more details about what you're trying to do and the change you would like to see?

— Reply to this email directly, view it on GitHub https://github.com/Azure/azure-sdk-for-go/issues/22818#issuecomment-2089238793, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAA7J577WULQ4QYXOWQ73BLZAFUOXAVCNFSM6AAAAABHCSCGSGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAOBZGIZTQNZZGM . You are receiving this because you were mentioned.Message ID: @.***>

chlowell commented 6 months ago

You're right that ClientSecretCredential isn't for delegation. You would use it when you want your app to access resources as itself. It sounds like you want Entra's on-behalf-of flow instead, documented here. A user would authenticate to your app, which would then exchange the user's access token for one allowing it to access resources on that user's behalf. Does that sound like what you're looking for? azidentity.OnBehalfOfCredential implements it.

github-actions[bot] commented 6 months ago

Hi @denen99. Thank you for opening this issue and giving us the opportunity to assist. To help our team better understand your issue and the details of your scenario please provide a response to the question asked above or the information requested above. This will help us more accurately address your issue.

denen99 commented 6 months ago

What should the value of userAssertion be in that method, is it the accessToken? If so, is it expected that the refresh_token is used to get an updated access_token via a different method?

here is my code:

cred, err := azidentity.NewOnBehalfOfCredentialWithSecret(tenant_id, client_id, access_token, client_secret, &azidentity.OnBehalfOfCredentialOptions{})
    if err != nil {
        fmt.Printf("Error creating OnBehalfOfCredential: %v\n", err)
        return
    }

client, err := msgraphsdkgo.NewGraphServiceClientWithCredentials(cred, []string{"Files.Read", "Files.Read.All", "offline_access"})
if err != nil {
        fmt.Printf("Error creating GraphServiceClient: %v\n", err)
        return
}

result, err := client.Me().Drive().Get(context.Background(), nil)
if err != nil {
        fmt.Printf("Error getting drive: %v\n", err)
        return
}

fmt.Printf("Drive ID: %v\n", *result.GetId())

But its throwing the following error (it works fine when using the HTTP URL raw)

Error getting drive: OnBehalfOfCredential authentication failed

POST https://login.microsoftonline.com/common/oauth2/v2.0/token

RESPONSE 400 Bad Request

{ "error": "invalid_grant", "error_description": "AADSTS50013: Assertion failed signature validation. [Reason - Key was found, but use of the key to verify the signature failed., Thumbprint of key used by client: 'REDACTED’, Found key 'Start=04/11/2024 16:04:26, End=04/11/2029 16:04:26', Please visit the Azure Portal, Graph Explorer or directly use MS Graph to see configured keys for app Id '00000000-0000-0000-0000-000000000000'. Review the documentation at https://docs.microsoft.com/en-us/graph/deployments to determine the corresponding service endpoint and https://docs.microsoft.com/en-us/graph/api/application-get?view=graph-rest-1.0&tabs=http to build a query request URL, such as 'https://graph.microsoft.com/beta/applications/00000000-0000-0000-0000-000000000000']. Trace ID: cd572808-d568-42e7-8470-38e69494a800 Correlation ID: 5d5362c2-940a-43e7-962c-207ac7d75426 Timestamp: 2024-05-02 13:16:04Z", "error_codes": [ 50013 ], "timestamp": "2024-05-02 13:16:04Z", "trace_id": "cd572808-d568-42e7-8470-38e69494a800", "correlation_id": "5d5362c2-940a-43e7-962c-207ac7d75426", "error_uri": "https://login.microsoftonline.com/error?code=50013" }

chlowell commented 6 months ago

Your code looks right to me though I'm not familiar with Graph or its SDK, so I may miss something there. userAssertion is the user's access token for your app. OnBehalfOfCredential handles the details of refreshing internally, redeeming the refresh token as needed. I suppose the error must be about the value of userAssertion because that becomes the assertion parameter of your app's token request. I haven't seen this error before but it's saying the signature on userAssertion doesn't verify. Is the value the user's access token for your app?

github-actions[bot] commented 6 months ago

Hi @denen99. Thank you for opening this issue and giving us the opportunity to assist. To help our team better understand your issue and the details of your scenario please provide a response to the question asked above or the information requested above. This will help us more accurately address your issue.

denen99 commented 6 months ago

Yes I believe the format of UserAssertion is what's wrong here but there is no way to figure out why.

This is the JSON returned from the /authorize endpoint when exchangin a Code for a token via the Oauth Code flow , i am using the value of the "access_token" field. There is no place to put the refresh_token which is why I was curious as to how this was handled automatically? Perhaps I am missing some detail on the structure of the UserAssertion string? Is there a method to construct this correctly? Its tricky if its a string and not a type/struct

{ "access_token": "REDACTED", "refresh_token": "REDACTED, "scope": "Files.Read Files.Read.All Files.Read.Selected User.Read profile openid email", "expires_in": 5227, "token_type": "Bearer" }

On Thu, May 2, 2024 at 1:29 PM Charles Lowell @.***> wrote:

Your code looks right to me though I'm not familiar with Graph or its SDK, so I may miss something there. userAssertion is the user's access token for your app. OnBehalfOfCredential handles the details of refreshing internally, redeeming the refresh token as needed. I suppose the error must be about the value of userAssertion because that becomes the assertion parameter of your app's token request. I haven't seen this error before but it's saying the signature on userAssertion doesn't verify. Is the value the user's access token for your app?

— Reply to this email directly, view it on GitHub https://github.com/Azure/azure-sdk-for-go/issues/22818#issuecomment-2091124048, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAA7J53IAHWNGSAAWXF7LCLZAJZWJAVCNFSM6AAAAABHCSCGSGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAOJRGEZDIMBUHA . You are receiving this because you were mentioned.Message ID: @.***>

chlowell commented 6 months ago

refresh_token is for the user to acquire other access tokens and isn't relevant to delegation. userAssertion would be the value of access_token. However, I see from the scopes that this access token is for Graph? It should be for your app (i.e., have your app as its audience), not the upstream resource you want the user to delegate access to. In this protocol diagram, your application is "Web API A" and Graph is "Web API B". The user acquires an access token for your app and presents it to your app e.g. as part of some HTTP request. That's step 1. OnBehalfOfCredential handles steps 2 and 3; the user's access token for your app is its userAssertion.

denen99 commented 6 months ago

Understood. That is exactly what I am doing though.

User goes through the Oauth code flow, redirects to my app with a ?code, query param. I then exchange that code for an acess_token , and securely store the access_token and refresh_token in my backend.

Then I load that access_token, pass it into the NewOnBehalf.. method as I pasted but it still returns the same error. Not sure what you mean that the access_token is for Graph? Its the access_token returned via the Oauth Code Flow for the user and is what is passed to the UserAssertion parameter.

Sorry if I am missing something obvious here

On Thu, May 2, 2024 at 4:19 PM Charles Lowell @.***> wrote:

refresh_token is for the user to acquire other access tokens and isn't relevant to delegation. userAssertion would be the value of access_token. However, I see from the scopes that this access token is for Graph? It should be for your app (i.e., have your app as its audience), not the upstream resource you want the user to delegate access to. In this protocol diagram https://learn.microsoft.com/entra/identity-platform/v2-oauth2-on-behalf-of-flow#protocol-diagram, your application is "Web API A" and Graph is "Web API B". The user acquires an access token for your app and presents it to your app e.g. as part of some HTTP request. That's step 1. OnBehalfOfCredential handles steps 2 and 3; the user's access token for your app is its userAssertion.

— Reply to this email directly, view it on GitHub https://github.com/Azure/azure-sdk-for-go/issues/22818#issuecomment-2091485726, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAA7J53HTQGBBOS5UZLD4KDZAKNVTAVCNFSM6AAAAABHCSCGSGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAOJRGQ4DKNZSGY . You are receiving this because you were mentioned.Message ID: @.***>

chlowell commented 6 months ago

No worries, this is a confusing scenario. In your last comment you shared what looks like the body of a response to an authentication request. The "scope" field documents what the access token is for. Those look like Graph scopes to me. If they are, the token is for accessing Graph, not your application, and you couldn't use it for delegation. What scopes do you request in the auth code flow?

denen99 commented 6 months ago

Sorry i figured this was a standard use case where a refresh_token is persisted on the backend and then used to query services on behalf of a user when getting an updated access_token

The flow is initiated with this URL for Files.Read, Files.Read.All and offline_access (for the refresh_token)

https://login.microsoftonline.com/MY_TENANT_ID/oauth2/v2.0/authorize https://login.microsoftonline.com/68de924b-91e1-40fa-a91c-778efb633fff/oauth2/v2.0/authorize ?client_id=MY_APP_CLIENT_ID&scope= https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=%7Bclient_id%7D&scope=%7Bscope%7D files.read%20files.read.all%20offline_access&response_type=code&redirect_uri= http://localhost:8080/oauth/msft_callback

That redirects back to http://localhost:8080/oauth/msft_callback?code=[authorization_code] , i then POST to "https://login.microsoftonline.com/common/oauth2/v2.0/token" with the following params to get that JSON payload back which has access_token and refresh_token with token of type Bearer

formData := url.Values{ "grant_type": {"authorization_code"}, "client_id": {client_id}, "client_secret": {client_secret}, "redirect_uri": {redirect_uri}, "code": {authorizationCode}, }

From the returned json i use the value of access_token to pass to the NewOnBehalfOfUser param

My goal is to have a backend application that runs every 15 minutes, uses the refresh_token to get a new access_token, and then queries the users OneDrive with the msgraphsdk, but the msgraphsdk requires a cred parameter that has me stuck here on how to properly construct from azidentity

On Thu, May 2, 2024 at 4:57 PM Charles Lowell @.***> wrote:

No worries, this is a confusing scenario. In your last comment you shared what looks like the body of a response to an authentication request. The "scope" field documents what the access token is for. Those look like Graph scopes to me. If they are, the token is for accessing Graph, not your application, and you couldn't use it for delegation. What scopes do you request in the auth code flow?

— Reply to this email directly, view it on GitHub https://github.com/Azure/azure-sdk-for-go/issues/22818#issuecomment-2091554313, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAA7J56TPXCXW5TTDS2FYSDZAKSCTAVCNFSM6AAAAABHCSCGSGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDAOJRGU2TIMZRGM . You are receiving this because you were mentioned.Message ID: @.***>

chlowell commented 6 months ago

I'm sorry, I assumed your app runs on a server, but I notice you're redirecting to localhost with the authorization code. Does your app run on a user's machine?

denen99 commented 6 months ago

No purely server based. http://localhost:8080 was just bc i was testing locally.

Frontend app redirects to msft login url, user authorizes app, MSFT redirects to redirect_uri with ?code, i exchange code for token and store token securely

On Thu, May 9, 2024 at 8:19 PM Charles Lowell @.***> wrote:

I'm sorry, I assumed your app runs on a server, but I notice you're redirecting to localhost with the authorization code. Does your app run on a user's machine?

— Reply to this email directly, view it on GitHub https://github.com/Azure/azure-sdk-for-go/issues/22818#issuecomment-2103637122, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAA7J54MX6WUWXX24SS6ZPLZBQG7NAVCNFSM6AAAAABHCSCGSGVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDCMBTGYZTOMJSGI . You are receiving this because you were mentioned.Message ID: @.***>

chlowell commented 6 months ago

Got it, thanks for clarifying. I asked about this because client apps should use a different solution.

Returning to authorization code vs. on-behalf-of (OBO), one key difference is that OBO separates a user's authorization to access an app from that app's authorization to access other APIs. In your case this means OBO would separate a user's authorization to access your app from your app's authorization to access Graph on that user's behalf. In the OBO flow a user would acquire an access token for your app, not for Graph, and your app would later exchange that token for an access token for Graph. OnBehalfOfCredential handles this exchange and the user's access token for your app is the userAssertion for its constructors. Currently you're using the auth code flow to acquire an access token for Graph directly--I guess your app has a totally separate login flow or none at all?--and that token doesn't work as the userAssertion because it's for Graph, not your application.

chlowell commented 4 months ago

I'm closing this because we don't plan to support acquiring and redeeming auth codes separately from InteractiveBrowserCredential, which does that internally for public clients. For confidential clients such as web APIs, OnBehalfOfCredential implements delegated authentication using a different flow. If you must use the auth code flow for a confidential client, please explain why the on-behalf-of flow isn't a workable substitute and consider using the MSAL for Go library. It supports acquiring and redeeming auth codes for confidential clients.