Skip to content

Token endpoint scope parameter (avoiding ADSTS28003) #130

@saper

Description

@saper

In a relatively obscure passage in RFC6749 it seems that token endpoint may also accept scope parameter:

[3.3](https://www.rfc-editor.org/rfc/rfc6749#section-3.3).  Access Token Scope

The authorization and token endpoints allow the client to specify the
scope of the access request using the "scope" request parameter.  In
turn, the authorization server uses the "scope" response parameter to
inform the client of the scope of the access token issued.

When requesting multiple scopes for multiple audiences, Microsoft Entra refuses the authorization code to access token exchange with the message

ADSTS28003: Provided value for the input parameter scope cannot be empty
when requesting an access token using the provided authorization code

The application is supposed to re-use the authorization code (or use the refresh token if issued) and call the token endpoint multiple times to obtain multiple access tokens, one per requested scope.

I wonder what should be the usable user interface for this in a tool like oauth2c. MSAL does it in a pretty ugly way (using an extraextra_scopes_to_consent parameter).

Looks like I can't call the token endpoint two times with the same authorization code:

AADSTS54005: OAuth2 Authorization code was already redeemed,
please retry with a new valid code or use an existing refresh token. 

The following sequence of curl requests seems to work:

Obtain the authorization code (token endpoint call will fail, but we use the authorization code)

oauth2c "$URL" \
  --no-browser \
  --client-id "$CLIENT_ID" \
  --response-types code \
  --response-mode query \
  --grant-type authorization_code \
  --auth-method none \
  --scopes scope1,scope2 \
  --authorization-endpoint "${AUTHORIZATION_ENDPOINT}" \
  --token-endpoint "${TOKEN_ENDPOINT}" \
  --pkce

Exchange authorization code for scope1 only

curl "${TOKEN_ENDPOINT}" \
 -H "Origin: http://localhost:9876" \
 -d redirect_uri=http://localhost:9876/callback \
 -d  code="$code" \
 -d code_verifier="$v" \
 -d grant_type=authorization_code \
 -d client_id=$CLIENT_ID \
 -d scope=scope1 > token1
REFRESH_TOKEN="$(jq -r .refresh_token < token1)"

Exchange the refresh token for scope2 only

curl "${TOKEN_ENDPOINT}" \
 -H "Origin: http://localhost:9876" \
 -d refresh_token="$REFRESH_TOKEN" \
 -d grant_type=refresh_token \
 -d client_id=$CLIENT_ID \
 -d scope=scope2 > token2

Refresh token for scope1

R1=$(jq -r .refresh_token token1)
oauth2c "$URL" \
  --no-browser \
  --client-id "$CLIENT_ID" \
  --grant-type refresh_token \
  --token-endpoint "${TOKEN_ENDPOINT}"  \
  --auth-method none \
  --refresh-token "$R1"

Refresh token for scope2

R2=$(jq -r .refresh_token token2)
oauth2c "$URL" \
  --no-browser \
  --client-id "$CLIENT_ID" \
  --grant-type refresh_token \
  --token-endpoint "${TOKEN_ENDPOINT}"  \
  --auth-method none \
  --refresh-token "$R2"

It looks like the refresh token "knows" which scope it should generate the updated access token for, but it is possible to specify the scope parameter anyway:

R2=$(jq -r .refresh_token token2)
oauth2c "$URL" \
  --no-browser \
  --client-id "$CLIENT_ID" \
  --grant-type refresh_token \
  --token-endpoint "${TOKEN_ENDPOINT}"  \
  --auth-method none \
  --scope scope1 \
  --refresh-token "$R2"

This also works and generates a new access token for scope1 and so on...

One implementation idea would be:

  1. We could add --token-scopes parameter (the actual name of the parameter would be scope) which could be added automatically for any calls to the token endpoint.

  2. The --grant_type authorization_code could send --scopes to the authorization endpoint and --token-scopes to the token endpoint (both as the scope parameter).

  3. The --grant_type refresh_token could send --token-scopes to the token endpoint.

  4. Implicit grant never calls the token endpoint, so the --token-scopes could be ignored (or not accepted)

  5. Client credentials - I don't know - in that case --scopes and --token-scopes mean the same.

  6. Other flows - no idea yet.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions