Nerzal / gocloak

golang keycloak client
Apache License 2.0
1.03k stars 283 forks source link

Cannot exchange token without impersonating #342

Closed quantonganh closed 2 years ago

quantonganh commented 2 years ago

To Reproduce Steps to reproduce the behavior:

  1. Create 2 clients:
    • public for frontend
    • confidential for backend
  2. Get the access token of the frontend
  3. Exchange with the backend client
  4. See error

The thing is requested_object is optional

requested_subject OPTIONAL. This specifies a username or user id if your client wants to impersonate a different user.

but we always pass it as a token option: https://github.com/Nerzal/gocloak/blob/main/client.go#L544:

// LoginClientTokenExchange will exchange the presented token for a user's token
// Requires Token-Exchange is enabled: https://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange
func (client *gocloak) LoginClientTokenExchange(ctx context.Context, clientID, token, clientSecret, realm, targetClient, userID string) (*JWT, error) {
    return client.GetToken(ctx, realm, TokenOptions{
        ClientID:           &clientID,
        ClientSecret:       &clientSecret,
        GrantType:          StringP("urn:ietf:params:oauth:grant-type:token-exchange"),
        SubjectToken:       &token,
        RequestedTokenType: StringP("urn:ietf:params:oauth:token-type:refresh_token"),
        Audience:           &targetClient,
        RequestedSubject:   &userID,
    })
}

What should I do if I don't want to impersonate a different user? Either I enter an username/userID:

$ curl -s -X POST \
                 -d "client_id=admin-panel-frontend" \
                 -d "client_secret=" \
                 --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
                 -d "subject_token=$access_token" \
                 --data-urlencode "requested_token_type=urn:ietf:params:oauth:token-type:refresh_token" \
                 -d "audience=admin-panel-backend" \
                 -d "requested_subject=alice" \
                 http://192.168.1.31:8080/auth/realms/nanos/protocol/openid-connect/token | jq
{
  "error": "access_denied",
  "error_description": "Client not allowed to exchange"
}

Or leave it as blank:

$ curl -s -X POST \
                 -d "client_id=admin-panel-frontend" \
                 -d "client_secret=" \
                 --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
                 -d "subject_token=$access_token" \
                 --data-urlencode "requested_token_type=urn:ietf:params:oauth:token-type:refresh_token" \
                 -d "audience=admin-panel-backend" \
                 -d "requested_subject=" \
                 http://192.168.1.31:8080/auth/realms/nanos/protocol/openid-connect/token | jq
{
  "error": "access_denied",
  "error_description": "Client not allowed to exchange"
}

It only works if I don't specify that param:

$ curl -s -X POST \
                 -d "client_id=admin-panel-frontend" \
                 -d "client_secret=" \
                 --data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
                 -d "subject_token=$access_token" \
                 --data-urlencode "requested_token_type=urn:ietf:params:oauth:token-type:refresh_token" \
                 -d "audience=admin-panel-backend" \
                 http://192.168.1.31:8080/auth/realms/nanos/protocol/openid-connect/token | jq
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJFMUZUVlpNbVFQN1ZzNTJSRlo0TTZ2QmtLY3