ory / hydra

The most scalable and customizable OpenID Certified™ OpenID Connect and OAuth Provider on the market. Become an OpenID Connect and OAuth2 Provider over night. Broad support for related RFCs. Written in Go, cloud native, headless, API-first. Available as a service on Ory Network and for self-hosters.
https://www.ory.sh/?utm_source=github&utm_medium=banner&utm_campaign=hydra
Apache License 2.0
15.5k stars 1.49k forks source link

Cannot exchange external OIDC ID token for Hydra access token due to `aud` claim handling in Hydra #3723

Open davidallendj opened 7 months ago

davidallendj commented 7 months ago

Preflight checklist

Ory Network Project

No response

Describe the bug

Trying to acquire an access token from Hydra using the urn:ietf:params:oauth:grant-type:jwt-bearer grant type fails when including an JWT from GitLab as the identity provider when making a request to the /oauth2/token. The intent is to receive an ID token through social sign-in with GitLab and then have Hydra respond with an access token upon successful verification of the JWT. I was basically following the docs for using JWTs as authorization grants. This process is automated with a custom tool called opaal.

The entire flow is broken down as follows:

  1. opaal initiates an authorization code flow by redirecting user to GitLab (assuming there's a registered client)

  2. User logs in and authorizes application with set scopes

  3. GitLab redirects back to opaal endpoint with code

  4. opaal makes another request back to GitLab with code to receive token (only interested in ID token)

  5. opaal fetches the JWKS from GitLab to include JWK in request to trust issuer

  6. The GitLab instance is added as a trusted issuer with the /admin/trust/grants/jwt-bearer/issuers endpoint

  7. A client is created/registered with Hydra with client ID, secret, and urn:ietf:params:oauth:grant-type:jwt-bearer grant type

  8. Request is made to /oauth2/token endpoint with grant type, client ID, and assertion

The expected output is another token from Hydra, but Hydra is complaining about the token having the wrong audience (see error below), which I believe is the intended behavior according to section 3.3 of RFC 7523 and the OpenID documentation. How would you go about using a JWT from another OIDC provider with Hydra if the provider sets the audience to something other than the intended token endpoint for Hydra?

Reproducing the bug

  1. Register OAuth2 app with GitLab (or any OIDC identity provider??)

  2. Run opaal with appropriate configuration file

  3. Login and consent to app access scopes (returns code used by opaal)

  4. Observe output from opaal

Alternatively to running opaal, run the following instead after getting token:

  1. Add trusted issuer: curl http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers -X POST -H 'Content-Type: application/json' -d '{"allow_any_subject": false, "issuer": "https://gitlab.com", "subject": "1", "expires_at": "2025-01-01 12:00:00", "jwk": "$jwk", "scope": ["openid", "profile", "email"]}'

  2. Register new client: curl http://127.0.0.1:4444/oauth2/client -X POST -H 'Content-Type: application/json' -d '{"client_name": "opaal", "token_endpoint_auth_method": "client_secret_post", "scope": "openid email profile", "grant_types": ["client_credentials", "urn:ietf:params:oauth:grant-type:jwt-bearer"], "response_type": ["token"]}'

  3. Make a request for a token: curl http://127.0.0.1:4444/oauth2/token -H 'Content-Type: application/x-www-form-urlencoded' -d 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&client_id=43b425a7-c687-4e20-92d0-c5f6ca81631e&client_secret=gloas-d478a49ce2e99a03f37d4324aa68832fbfb9d0712f92493c42a5db13b4540299&scope=openid+profile+email&assertion=${jwt}'

Relevant log output

{
  "error": "invalid_grant",
  "error_description": "The provided authorization grant (e.g., authorization code, resource owner credentials) or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The JWT in assertion request parameter MUST contain an aud (audience) claim containing a value http://127.0.0.1:4444/oauth2/token that identifies the authorization server as an intended audience."
}

Relevant configuration

# Hydra config:

serve:
  cookies:
    same_site_mode: Lax
    names:
      login_csrf: login
      consent_csrf: consent
      session: session

urls:
  self:
    issuer: http://127.0.0.1:4444
  consent: http://127.0.0.1:4455/consent
  login: http://127.0.0.1:4455/login
  logout: http://127.0.0.1:4455/logout

secrets:
  system:
    - <REDACTED>

oidc:
  dynamic_client_registration:
    enabled: true
  subject_identifiers:
    supported_types:
      - pairwise
      - public
    pairwise:
      salt: <REDACTED>

oauth2:
  grant:
    jwt:
      jti_optional: true
      iat_optional: true
      max_ttl: 1h

log:
  leak_sensitive_values: true

# Docker compose

version: "3.7"

networks:
  internal:
  external:
    external: true

volumes:
  hydra-sqlite:

services:
  hydra:
    image: oryd/hydra:v2.2.0
    container_name: hydra
    ports:
      - "4444:4444" # Public port
      - "4445:4445" # Admin port
      - "5555:5555" # Port for hydra token user
    command: serve -c /etc/config/hydra/hydra.yml all --dev
    volumes:
      - type: volume
        source: hydra-sqlite
        target: /var/lib/sqlite
        read_only: false
      - type: bind
        source: ./configs/hydra
        target: /etc/config/hydra
    environment:
      - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true
    restart: unless-stopped
    depends_on:
      - hydra-migrate
    networks:
      - internal
  hydra-migrate:
    image: oryd/hydra:v2.2.0
    environment:
      - DSN=sqlite:///var/lib/sqlite/db.sqlite?_fk=true
    command: migrate -c /etc/config/hydra/hydra.yml sql -e --yes
    volumes:
      - type: volume
        source: hydra-sqlite
        target: /var/lib/sqlite
        read_only: false
      - type: bind
        source: ./configs/hydra
        target: /etc/config/hydra
    restart: on-failure
    networks:
      - internal
  consent:
    environment:
      - HYDRA_ADMIN_URL=http://hydra:4445
    image: oryd/hydra-login-consent-node:v2.2.0
    ports:
      - "3001:3001"
    restart: unless-stopped
    networks:
      - internal

# Opaal's config:

server:  
  host: 127.0.0.1
  port: 3333
client: 
  id: <REDACTED>        # from GitLab applications
  secret: <REDACTED>    # from GitLab applications
  redirect-uris:
    - "http://127.0.0.1:3333/oidc/callback"
oidc:
  issuer: "https://gitlab.com"
urls:
  #identities: http://127.0.0.1:4434/admin/identities
  trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers
  access-token: http://127.0.0.1:4444/oauth2/token
  server-config: https://gitlab.com/.well-known/openid-configuration
  jwks_uri: https://gitlab.com/oauth/discovery/keys
  login: http://127.0.0.1:4433/self-service/login/api
  login-flow-id: http://127.0.0.1:4433/self-service/login/flows?id={id}
state: ""
response-type: code
decode-id-token: true
decode-access-token: true
run-once: true
scope:
  - openid
  - profile
  - email

Version

2.2.0

On which operating system are you observing this issue?

macOS

In which environment are you deploying?

Docker Compose

Additional Context

No response

Editted links and formatting

davidallendj commented 7 months ago

Just to add, something like RFC 8707 should make what I'm trying to do possible, but I think that would have to be implemented on the identity provider side (in this case GitLab) to specify the intended audience.