Skip to content
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

DRAFT: Add Token Exchange Grant Type (RFC8693) #1052

Draft
wants to merge 25 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7267da4
feat: first stab at tests
blairdrummond Jan 2, 2024
86fa875
fix: fix github id_token
blairdrummond Jan 2, 2024
b7d784b
fix: fix dex cmd
blairdrummond Jan 2, 2024
3c8222f
ci: add more logging
blairdrummond Jan 2, 2024
fa9c84f
feat: add tailscale step for debugging
blairdrummond Jan 2, 2024
612b8f6
feat: add tailscale step for debugging
blairdrummond Jan 2, 2024
b75b7c7
fix: capture env vars
blairdrummond Jan 2, 2024
e97dc15
fix: remove setup
blairdrummond Jan 2, 2024
a31b89f
fix: add ID_TOKEN var
blairdrummond Jan 2, 2024
7ddd7ed
fix: sleep for 30m
blairdrummond Jan 2, 2024
709d9b6
fix: set BROWSER in workflow
blairdrummond Jan 2, 2024
7b9e1d7
fix: add debug logging
blairdrummond Jan 3, 2024
6ad5caf
refactor: split up the tests
blairdrummond Jan 3, 2024
1d789fc
fix: add clusterrolebinding stuff for token-exchange
blairdrummond Jan 3, 2024
37019d2
fix: try adding email claim
blairdrummond Jan 4, 2024
db0baa5
fix: try adding email claim
blairdrummond Jan 4, 2024
8d746d0
fix: fix scopes stuff
blairdrummond Jan 4, 2024
a806f26
fix: scope and binary stuff
blairdrummond Jan 4, 2024
ca6ac1d
fix: small things
blairdrummond Jan 4, 2024
fa07c0c
fix: let me in
blairdrummond Jan 4, 2024
85e9851
fix: remove debug
blairdrummond Jan 4, 2024
ff5788c
feat: added extra param options for token-exchange
jknight-liatrio Jan 4, 2024
0081092
fix: fix for authRequestExtraParams renaming
jknight-liatrio Jan 4, 2024
65975d2
fix: range over wrong var
blairdrummond Jan 4, 2024
6eeeb60
Merge pull request #1 from blairdrummond/feat-rfc8693
blairdrummond Feb 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 89 additions & 3 deletions .github/workflows/system-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ on:
- go.*

jobs:
system-test:
system-test-authorization-code:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand All @@ -30,12 +30,98 @@ jobs:
# for certutil
# https://packages.ubuntu.com/xenial/libnss3-tools
- run: sudo apt update
- run: sudo apt install -y libnss3-tools
- run: sudo apt install -y libnss3-tools curl
- run: mkdir -p ~/.pki/nssdb

- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts

- run: make -C system_test -j3
- run: make -C system_test -j3 authorization_code
env:
BROWSER: chromelogin

- run: make -C system_test logs
if: always()

- name: Tailscale
if: ${{ failure() && !cancelled() }}
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:runner
args: "--ssh"

- name: Tailscale Sleep
if: ${{ failure() && !cancelled() }}
run: |
printenv > /tmp/runner_env
sleep 1800
env:
ID_TOKEN: ${{steps.tokenid.outputs.idToken}}

system-test-token-exchange:
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: 1.21.5

# for certutil
# https://packages.ubuntu.com/xenial/libnss3-tools
- run: sudo apt update
- run: sudo apt install -y libnss3-tools curl
- run: mkdir -p ~/.pki/nssdb

# Get Github ID_TOKEN
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-cloud-providers#requesting-the-jwt-using-environment-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: |
IDTOKEN=$(curl -H "Authorization: bearer ${{steps.script.outputs.TOKEN}}" ${{steps.script.outputs.IDTOKENURL}} -H "Accept: application/json; api-version=2.0" -H "Content-Type: application/json" -d "{}" | jq -r '.value')
echo $IDTOKEN
jwtd() {
if [[ -x $(command -v jq) ]]; then
jq -R 'split(".") | .[0],.[1] | @base64d | fromjson' <<< "${1}"
fi
}
jwtd $IDTOKEN
echo "idToken=${IDTOKEN}" >> $GITHUB_OUTPUT
id: tokenid

- run: echo '127.0.0.1 dex-server' | sudo tee -a /etc/hosts

- run: make -C system_test -j3 token_exchange
env:
ID_TOKEN: ${{steps.tokenid.outputs.idToken}}

- run: make -C system_test logs
if: always()

- name: Tailscale
if: ${{ failure() && !cancelled() }}
uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:runner
args: "--ssh"

- name: Tailscale Sleep
if: ${{ failure() && !cancelled() }}
run: |
printenv > /tmp/runner_env
sleep 1800
env:
ID_TOKEN: ${{steps.tokenid.outputs.idToken}}
3 changes: 3 additions & 0 deletions integration_test/oidcserver/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ func (h *Handler) serveHTTP(w http.ResponseWriter, r *http.Request) error {
if err := e.Encode(tokenResponse); err != nil {
return fmt.Errorf("could not render json: %w", err)
}
case "urn:ietf:params:oauth:grant-type:token-exchange":
// TODO
return fmt.Errorf("TODO implement token-exchange")
default:
// 5.2. Error Response
// https://tools.ietf.org/html/rfc6749#section-5.2
Expand Down
37 changes: 36 additions & 1 deletion pkg/cmd/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/devicecode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/int128/kubelogin/pkg/usecases/authentication/tokenexchange"
"github.com/spf13/pflag"
)

Expand All @@ -29,6 +30,15 @@ type authenticationOptions struct {
AuthRequestExtraParams map[string]string
Username string
Password string

TokenExchangeResource string
TokenExchangeAudience string
TokenExchangeRequestedTokenType string
TokenExchangeSubjectToken string
TokenExchangeSubjectTokenType string
TokenExchangeBasicAuth bool
TokenExchangeActorToken string
TokenExchangeActorTokenType string
}

// determineListenAddress returns the addresses from the flags.
Expand All @@ -52,6 +62,7 @@ var allGrantType = strings.Join([]string{
"authcode-keyboard",
"password",
"device-code",
"token-exchange",
}, "|")

func (o *authenticationOptions) addFlags(f *pflag.FlagSet) {
Expand All @@ -70,9 +81,17 @@ func (o *authenticationOptions) addFlags(f *pflag.FlagSet) {
f.StringVar(&o.OpenURLAfterAuthentication, "open-url-after-authentication", "", "[authcode] If set, open the URL in the browser after authentication")
f.StringVar(&o.RedirectURLHostname, "oidc-redirect-url-hostname", "localhost", "[authcode] Hostname of the redirect URL")
f.StringVar(&o.RedirectURLAuthCodeKeyboard, "oidc-redirect-url-authcode-keyboard", oobRedirectURI, "[authcode-keyboard] Redirect URL")
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "[authcode, authcode-keyboard] Extra query parameters to send with an authentication request")
f.StringToStringVar(&o.AuthRequestExtraParams, "oidc-auth-request-extra-params", nil, "[authcode, authcode-keyboard, token-exchange] Extra query parameters to send with an authentication request")
f.StringVar(&o.Username, "username", "", "[password] Username for resource owner password credentials grant")
f.StringVar(&o.Password, "password", "", "[password] Password for resource owner password credentials grant")
f.StringVar(&o.TokenExchangeResource, "token-exchange-resource", "", "[token-exchange] a URI for the target resource the client intends to use")
f.StringVar(&o.TokenExchangeAudience, "token-exchange-audience", "", "[token-exchange] the audience the client intends to use (default: client-id)")
f.StringVar(&o.TokenExchangeRequestedTokenType, "token-exchange-requested-token-type", "", "[token-exchange] return type desired in response, e.g. id-token or access-token")
f.StringVar(&o.TokenExchangeSubjectToken, "token-exchange-subject-token", "", "[token-exchange] the token to exchange (required)")
f.StringVar(&o.TokenExchangeSubjectTokenType, "token-exchange-subject-token-type", "", "[token-exchange] the type of token provided, e.g. id-token or access-token (required)")
f.BoolVar(&o.TokenExchangeBasicAuth, "token-exchange-basic-auth", false, "[token-exchange] use basic auth for exchanging the token (default: false)")
f.StringVar(&o.TokenExchangeActorToken, "token-exchange-actor-token", "", "[token-exchange] optional token for delegated access pattern")
f.StringVar(&o.TokenExchangeActorTokenType, "token-exchange-actor-token-type", "", "[token-exchange] type of the actor token, e.g. id-token or access-token")
}

func (o *authenticationOptions) expandHomedir() {
Expand Down Expand Up @@ -109,6 +128,22 @@ func (o *authenticationOptions) grantOptionSet() (s authentication.GrantOptionSe
SkipOpenBrowser: o.SkipOpenBrowser,
BrowserCommand: o.BrowserCommand,
}
case o.GrantType == "token-exchange":

var tokenExchangeOpts *tokenexchange.Option
tokenExchangeOpts, err = tokenexchange.NewTokenExchangeOption(
o.TokenExchangeSubjectToken,
o.TokenExchangeSubjectTokenType,
tokenexchange.AddAudience(o.TokenExchangeAudience),
tokenexchange.AddRequestedTokenType(o.TokenExchangeRequestedTokenType),
tokenexchange.AddResource(o.TokenExchangeResource),
tokenexchange.SetBasicAuth(o.TokenExchangeBasicAuth),
tokenexchange.AddActorToken(o.TokenExchangeActorToken, o.TokenExchangeActorTokenType),
tokenexchange.AddExtraParams(o.AuthRequestExtraParams),
)

s.TokenExchangeOption = tokenExchangeOpts

default:
err = fmt.Errorf("grant-type must be one of (%s)", allGrantType)
}
Expand Down
12 changes: 12 additions & 0 deletions pkg/oidc/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ type GetTokenByAuthCodeInput struct {
LocalServerKeyFile string
}

// // https://datatracker.ietf.org/doc/html/rfc8693#name-token-exchange-request-and-
// type TokenExchangeInput struct {
// ResourceURI string
// Audience string
// Scope string
// SubjectToken string
// SubjectTokenType string
// ActorToken string
// ActorTokenType string
// RequestedTokenType string
// }

type client struct {
httpClient *http.Client
provider *gooidc.Provider
Expand Down
11 changes: 11 additions & 0 deletions pkg/usecases/authentication/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/int128/kubelogin/pkg/usecases/authentication/authcode"
"github.com/int128/kubelogin/pkg/usecases/authentication/devicecode"
"github.com/int128/kubelogin/pkg/usecases/authentication/ropc"
"github.com/int128/kubelogin/pkg/usecases/authentication/tokenexchange"
)

// Set provides the use-case of Authentication.
Expand All @@ -23,6 +24,7 @@ var Set = wire.NewSet(
wire.Struct(new(authcode.Keyboard), "*"),
wire.Struct(new(ropc.ROPC), "*"),
wire.Struct(new(devicecode.DeviceCode), "*"),
wire.Struct(new(tokenexchange.TokenExchange), "*"),
)

type Interface interface {
Expand All @@ -43,6 +45,7 @@ type GrantOptionSet struct {
AuthCodeKeyboardOption *authcode.KeyboardOption
ROPCOption *ropc.Option
DeviceCodeOption *devicecode.Option
TokenExchangeOption *tokenexchange.Option
}

// Output represents an output DTO of the Authentication use-case.
Expand Down Expand Up @@ -71,6 +74,7 @@ type Authentication struct {
AuthCodeKeyboard *authcode.Keyboard
ROPC *ropc.ROPC
DeviceCode *devicecode.DeviceCode
TokenExchange *tokenexchange.TokenExchange
}

func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
Expand Down Expand Up @@ -140,5 +144,12 @@ func (u *Authentication) Do(ctx context.Context, in Input) (*Output, error) {
}
return &Output{TokenSet: *tokenSet}, nil
}
if in.GrantOptionSet.TokenExchangeOption != nil {
tokenSet, err := u.TokenExchange.Do(ctx, in.GrantOptionSet.TokenExchangeOption, in.Provider)
if err != nil {
return nil, fmt.Errorf("token-exchange error: %w", err)
}
return &Output{TokenSet: *tokenSet}, nil
}
return nil, fmt.Errorf("any authorization grant must be set")
}
44 changes: 44 additions & 0 deletions pkg/usecases/authentication/identifiers/identifiers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Token types defined in RFC 8693. Defines token "formats" and "purposes"
// https://datatracker.ietf.org/doc/html/rfc8693#name-token-type-identifiers
package identifiers

import (
"fmt"
"strings"
)

const AccessTokenType = "urn:ietf:params:oauth:token-type:access_token"
const RefreshTokenType = "urn:ietf:params:oauth:token-type:refresh_token"
const IDTokenType = "urn:ietf:params:oauth:token-type:id_token"

const SAML1TokenType = "urn:ietf:params:oauth:token-type:saml1"
const SAML2TokenType = "urn:ietf:params:oauth:token-type:saml2"
const JWTTokenType = "urn:ietf:params:oauth:token-type:jwt"

// Given a string like "refresh-token", return "urn:ietf:params:oauth:token-type:refresh_token"
// Return the same string if already in canonical format.
// if the input string is not a known type, return an error
func CanonicalTokenType(s string) (string, error) {
known_types := []string{
AccessTokenType,
RefreshTokenType,
IDTokenType,
SAML1TokenType,
SAML2TokenType,
JWTTokenType,
}

for _, t := range known_types {
if s == t {
return t, nil
}

// refresh-token -> refresh_token
s = strings.Replace(s, "-", "_", -1)
if fmt.Sprintf("urn:ietf:params:oauth:token-type:%s", s) == t {
return t, nil
}
}

return "", fmt.Errorf("unknown token type: %s", s)
}
Loading