generated from cncf/hugo-netlify-starter
-
Notifications
You must be signed in to change notification settings - Fork 69
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
documentation for token exchanges #140
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,227 @@ | ||
--- | ||
title: "Machine Authentication to Dex" | ||
linkTitle: "Authentication for Machines" | ||
description: "" | ||
date: 2023-07-01 | ||
draft: false | ||
toc: true | ||
weight: 1080 | ||
--- | ||
|
||
## Overview | ||
|
||
Most Dex connectors redirect users to the upstream identity provider as part of the authentication flow. | ||
While this works for human users, | ||
it is much harder for machines and automated processes (e.g., CI pipelines) to complete this interactive flow. | ||
This is where [OAuth2 Token Exchange][token-exchange] comes in: | ||
it allows clients to exchange an access or ID token they already have | ||
(obtained from their environment, though custom CLI commands, etc.) | ||
for a token issued by dex. | ||
|
||
This works like [GCP Workload Identity Federation][gcp-federation] and [AWS Web Identity Federation][aws-federation], | ||
allowing processes running in trusted execution environments that issue OIDC tokens, | ||
such as [Gtihub Actions][gh-actions], [Buildkite][buildkite], [CircleCI][circleci], [GCP][gcp], and others, | ||
to exchange them for a dex issued token to access protected resources. | ||
|
||
The authentication flow looks like this: | ||
|
||
1. Client independently obtains an access / id token from the upstream IDP. | ||
2. Client exchanges the upstream token for a dex access / id token via the token exchange flow. | ||
3. Use token to access dex protected resources. | ||
4. Repeat these steps when the token expires. | ||
|
||
## Configuring dex | ||
|
||
Currently, only the [OIDC Connector][oidc-connector] supports token exchanges. | ||
For this flow, `clientID`, `clientSecret`, and `redirectURI` aren't required. | ||
`getUserInfo` is required if you want to exchange from access tokens to dex issued tokens. | ||
|
||
As the user performing the token exchange will need the client secret, | ||
we configure the client as a [public client](./custom-scopes-claims-clients.md#public-clients). | ||
If you need to allow humans and machines to authenticate, | ||
consider creating a dedicated public client for token exchange | ||
and using [cross-client trust](./custom-scopes-claims-clients.md#cross-client-trust-and-authorized-party). | ||
|
||
```yaml | ||
issuer: https://dex.example.com | ||
storage: | ||
type: sqlite3 | ||
config: | ||
file: dex.db | ||
web: | ||
http: 0.0.0.0:8001 | ||
|
||
outh2: | ||
grantTypes: | ||
# ensure grantTypes includes the token-exchange grant (default) | ||
- "urn:ietf:params:oauth:grant-type:token-exchange" | ||
|
||
connectors: | ||
- name: My Upstream | ||
type: oidc | ||
id: my-upstream | ||
config: | ||
# The client submitted subject token will be verified against the issuer given here. | ||
issuer: https://token.example.com | ||
# Additional scopes in token response, supported list at: | ||
# https://dexidp.io/docs/custom-scopes-claims-clients/#scopes | ||
scopes: | ||
- groups | ||
- federated:id | ||
# mapping of fields from the submitted token | ||
userNameKey: sub | ||
# Access tokens are generally considered opaque. | ||
# We check their validity by calling the user info endpoint if it's supported. | ||
# getUserInfo: true | ||
|
||
staticClients: | ||
# dex issued tokens are bound to clients. | ||
# For the token exchange flow, the client id and secret pair must be submitted as the username:password | ||
# via Basic Authentication. | ||
- name: My App | ||
id: my-app | ||
secret: my-secret | ||
# We set public to indicate we don't intend to keep the client secret actually secret. | ||
# https://dexidp.io/docs/custom-scopes-claims-clients/#public-clients | ||
public: true | ||
``` | ||
|
||
## Performing a token exchange | ||
|
||
To exchange an upstream IDP token for a dex issued token, | ||
perform an `application/x-www-form-urlencoded` `POST` request | ||
to dex's `/token` endpoint following [RFC 8693 Section 2.1][token-exchange-2-1]. | ||
Additionally, dex requires the connector to be specified with the `connector_id` parameter | ||
and a client id/secret to be included as the username/password via Basic Authentication. | ||
|
||
```sh | ||
$ export UPSTREAM_TOKEN=$(# get a token from the upstream IDP) | ||
|
||
$ curl https://dex.example.com/token \ | ||
--user my-app:my-secret \ | ||
--data-urlencode connector_id=my-upstream \ | ||
--data-urlencode grant_type=urn:ietf:params:oauth:grant-type:token-exchange \ | ||
--data-urlencode scope="openid groups federated:id" \ | ||
--data-urlencode requested_token_type=urn:ietf:params:oauth:token-type:access_token \ | ||
--data-urlencode subject_token=$UPSTREAM_TOKEN \ | ||
--data-urlencode subject_token_type=urn:ietf:params:oauth:token-type:access_token | ||
``` | ||
|
||
Below is an example of a successful response. | ||
Note that regardless of the `requested_token_type`, | ||
the token will always be in the `access_token` field, | ||
with the type indicated by the `issued_token_type` field. | ||
See [RFC 8693 Section 2.2.1][token-exchange-2-2-1] for details. | ||
|
||
```json | ||
{ | ||
"access_token":"eyJhbGciOi....aU5oA", | ||
"issued_token_type":"urn:ietf:params:oauth:token-type:access_token", | ||
"token_type":"bearer", | ||
"expires_in":86399 | ||
} | ||
``` | ||
|
||
### Full example with GitHub Actions | ||
|
||
Here is an example of running dex as a service during a Github Actions workflow | ||
and getting an access token from it, exchanged from a Github Actions OIDC token. | ||
|
||
Dex config: | ||
|
||
```yaml | ||
issuer: http://127.0.0.1:5556/ | ||
storage: | ||
type: sqlite3 | ||
config: | ||
file: dex.db | ||
web: | ||
http: 0.0.0.0:8080 | ||
connectors: | ||
- type: oidc | ||
id: github-actions | ||
name: github-actions | ||
config: | ||
issuer: https://token.actions.githubusercontent.com | ||
scopes: | ||
- openid | ||
- groups | ||
userNameKey: sub | ||
staticClients: | ||
- name: My app | ||
id: my-app | ||
secret: my-secret | ||
public: true | ||
``` | ||
|
||
Github actions workflow. | ||
Replace the service image with one that has the config included. | ||
|
||
```yaml | ||
name: workflow1 | ||
|
||
on: [push] | ||
|
||
permissions: | ||
id-token: write # This is required for requesting the JWT | ||
|
||
jobs: | ||
job: | ||
runs-on: ubuntu-latest | ||
services: | ||
dex: | ||
# replace with an image that has the config above | ||
image: ghcr.io/dexidp/dex:latest | ||
ports: | ||
- 80:8080 | ||
steps: | ||
# Actions have access to two special environment variables ACTIONS_CACHE_URL and ACTIONS_RUNTIME_TOKEN. | ||
# Inline step scripts in workflows do not see these variables. | ||
- uses: actions/github-script@v6 | ||
id: script | ||
timeout-minutes: 10 | ||
with: | ||
debug: true | ||
script: | | ||
const token = process.env['ACTIONS_RUNTIME_TOKEN'] | ||
const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL'] | ||
core.setOutput('TOKEN', token.trim()) | ||
core.setOutput('IDTOKENURL', runtimeUrl.trim()) | ||
- run: | | ||
# get an token from github | ||
GH_TOKEN_RESPONSE=$(curl \ | ||
"${{steps.script.outputs.IDTOKENURL}}" \ | ||
-H "Authorization: bearer ${{steps.script.outputs.TOKEN}}" \ | ||
-H "Accept: application/json; api-version=2.0" \ | ||
-H "Content-Type: application/json" \ | ||
-d "{}" \ | ||
) | ||
GH_TOKEN=$(jq -r .value <<< $GH_TOKEN_RESPONSE) | ||
|
||
# exchange it for a dex token | ||
DEX_TOKEN_RESPONSE=$(curl \ | ||
http://127.0.0.1/token \ | ||
--user my-app:my-secret \ | ||
--data-urlencode "connector_id=github-actions" \ | ||
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ | ||
--data-urlencode "scope=openid groups federated:id" \ | ||
--data-urlencode "requested_token_type=urn:ietf:params:oauth:token-type:access_token" \ | ||
--data-urlencode "subject_token=$GH_TOKEN" \ | ||
--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token") | ||
DEX_TOKEN=$(jq -r .access_token <<< $DEX_TOKEN_RESPONSE) | ||
|
||
# use $DEX_TOKEN | ||
|
||
id: idtoken | ||
``` | ||
|
||
[token-exchange]: https://www.rfc-editor.org/rfc/rfc8693.html | ||
[token-exchange-2-1]: https://www.rfc-editor.org/rfc/rfc8693.html#name-request | ||
[token-exchange-2-2-1]: https://www.rfc-editor.org/rfc/rfc8693.html#name-successful-response | ||
[gcp-federation]: https://cloud.google.com/iam/docs/workload-identity-federation | ||
[aws-federation]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html | ||
[gh-actions]: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect | ||
[buildkite]: https://badge.buildkite.com/docs/agent/v3/cli-oidc | ||
[circleci]: https://circleci.com/docs/openid-connect-tokens/ | ||
[gcp]: https://cloud.google.com/sdk/gcloud/reference/auth/print-access-token | ||
[oidc-connector]: https://dexidp.io/docs/connectors/oidc/ |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused by the presence of the
staticClients
block -- when I first read this, my mental model was that it would be necessary to declare clients (either in astaticClients
block or via the gRPC API) in order to conduct token exchange for them. However, in the example below that shows how token exchange can be used in Github Actions (which happens to be something I am working on right now, so this doc update is quite timely for me!) there are nostaticClients
configured in the Dex config, nor does there appear to be any interaction with Dex aside from the token exchange POST to the/token
endpoint. Could you clarify whether or not a client would need to be configured (throughstaticClients
or otherwise) in order to support token exchange for a given client?It seems that because the client ID and secret must be provided in the basic auth for the token exchange post, that perhaps Dex has all the information it needs without the client actually having been configured, but in that case I do not understand why the client is configured here, since this config seems to only allow token exchange. I understand it would be necessary to include the client when we want to implement both token exchange and user-mediated auth flows, and perhaps the purpose of including this block in the example is so you can include the comment regarding setting
public: true
because the existence of the token exchange flow indicates that the client secret is not really a secret, but at this point I feel I am guessing quite a lot and it would be very helpful for the documentation to clarify this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, i left out part of the config during the copy/paste, a static client is required.
Line 68 is correct.
Within dex, there are 2 separate entities: connectors (upstreams that authenticate users), and clients (downstreams that trigger authentication, and use the dex issued token). The tokens that dex issued are always bound to both: it's bound by which connector authenticated the user (exposed when the reequested scope includes
federated:id
, and which client is expected to consume the token (always exposed as audience /aud
).In a token exchange, dex has no prior information about how to bind the tokens (in a web flow, the app that triggers auth will identify itself as the client), so the user has to provide both: it identifies the connector with
connector_id
and it identifies the client with client id + client secret in basic auth.In a dual user/machine scenario, you'll likely want to make use of dex's cross client trust:
your consuming application is configured with a real pair of client id/secret, and trusts a public client for use with token exchange.
Given the above information, what would make the docs clearer (though I feel explaining connectors/clients is out of scope for this page).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that explaining connectors/clients should be out of scope for this page. I think my main confusion came from the missing config block, as I originally had the right idea that a (public) client needs to be configured.
I do think that since the use of token exchange necessitates treating the client as public as well as the use-cases involving token exchange being likely to be apps that also have real (i.e. human) users, it would be helpful to include the example (essentially from your comment) of how one might want to employ cross-client trust to trust the public client and then request the scope of the main application client. Or at a minimum, these docs could mention that with a reference to the main cross client trust docs without giving an example (though I would note that those docs do not seem to specifically address the use-case of trusting a public client, though perhaps the section on public clients would be a more appropriate place to include that as an option, and this doc could then just reference that).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a paragraph explaining why a public client is required, plus a link to cross client trust.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, that looks great.