Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The available flags are:
--actor-token string acting party token
--actor-token-type string acting party token type
--assertion string claims for jwt bearer assertion
--assertion-jwt string pre-signed jwt bearer assertion, passed as-is
--audience strings requested audience
--auth-method string token endpoint authentication method
--authentication-code string authentication code used for passwordless authentication
Expand Down Expand Up @@ -124,6 +125,7 @@ The available flags are:
--redirect-url string client redirect url (default "http://localhost:9876/callback")
--refresh-token string refresh token
--request-object pass request parameters as jwt
--requested-token-type string requested token type
--resource strings requested resource
--response-mode string response mode
--response-types strings response type
Expand Down
18 changes: 9 additions & 9 deletions cmd/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ func LogInputData(cc oauth2.ClientConfig) {
{"Password", cc.Password},
{"Refresh token", cc.RefreshToken},
{"Signing key", cc.SigningKey},
{"Requested token type", cc.RequestedTokenType},
{"Subject token type", cc.SubjectTokenType},
{"Actors token type", cc.ActorTokenType},
{"TLS client cert", cc.TLSCert},
Expand Down Expand Up @@ -327,18 +328,13 @@ func LogRequestObject(r oauth2.Request) {
LogJson(requestClaims)
pterm.Println()

if r.SigningKey != nil {
LogKey("Signing key", r.SigningKey)
}

if r.EncryptionKey != nil {
LogKey("Encryption key", r.EncryptionKey)
}
LogKey("Signing key", r.SigningKey)
LogKey("Encryption key", r.EncryptionKey)
}
}
}

func LogAssertion(request oauth2.Request, title string, name string) {
func LogAssertion(request oauth2.Request, title string, name string, signingKey interface{}) {
var (
assertion = request.Form.Get(name)
token *jwt.JSONWebToken
Expand All @@ -365,10 +361,14 @@ func LogAssertion(request oauth2.Request, title string, name string) {
LogJson(claims)
pterm.Println("")

LogKey("Signing key", request.SigningKey)
LogKey("Signing key", signingKey)
}

func LogKey(name string, key interface{}) {
Comment thread
mschaab-SA marked this conversation as resolved.
if key == nil {
return
}

var err error

pterm.Println(name)
Expand Down
2 changes: 2 additions & 0 deletions cmd/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ func NewOAuth2Cmd(version, commit, date string) (cmd *OAuth2Cmd) {
cmd.PersistentFlags().BoolVar(&cconfig.RequestObject, "request-object", false, "pass request parameters as jwt")
cmd.PersistentFlags().BoolVar(&cconfig.EncryptedRequestObject, "encrypted-request-object", false, "pass request parameters as encrypted jwt")
cmd.PersistentFlags().StringVar(&cconfig.Assertion, "assertion", "", "claims for jwt bearer assertion")
cmd.PersistentFlags().StringVar(&cconfig.AssertionJWT, "assertion-jwt", "", "pre-signed jwt bearer assertion, passed as-is")
cmd.PersistentFlags().StringVar(&cconfig.SigningKey, "signing-key", "", "path or url to signing key in jwks format")
cmd.PersistentFlags().StringVar(&cconfig.EncryptionKey, "encryption-key", "", "path or url to encryption key in jwks format")
cmd.PersistentFlags().StringVar(&cconfig.RequestedTokenType, "requested-token-type", "", "requested token type")
cmd.PersistentFlags().StringVar(&cconfig.SubjectToken, "subject-token", "", "third party token")
cmd.PersistentFlags().StringVar(&cconfig.SubjectTokenType, "subject-token-type", "", "third party token type")
cmd.PersistentFlags().StringVar(&cconfig.ActorToken, "actor-token", "", "acting party token")
Expand Down
2 changes: 1 addition & 1 deletion cmd/oauth2_authorize_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func (c *OAuth2Cmd) AuthorizationCodeGrantFlow(clientConfig oauth2.ClientConfig,
return err
}

LogAssertion(parRequest, "Client assertion", "client_assertion")
LogAssertion(parRequest, "Client assertion", "client_assertion", parRequest.ClientAssertionKey)
LogAuthMethod(clientConfig)
LogRequestObject(parRequest)
LogRequestAndResponse(parRequest, parResponse)
Expand Down
4 changes: 2 additions & 2 deletions cmd/oauth2_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ func (c *OAuth2Cmd) tokenEndpointFlow(
return err
}

LogAssertion(tokenRequest, "Assertion", "assertion")
LogAssertion(tokenRequest, "Client assertion", "client_assertion")
LogAssertion(tokenRequest, "Assertion", "assertion", tokenRequest.SigningKey)
LogAssertion(tokenRequest, "Client assertion", "client_assertion", tokenRequest.ClientAssertionKey)
LogSubjectTokenAndActorToken(tokenRequest)
LogAuthMethod(clientConfig)
LogRequestAndResponse(tokenRequest, tokenResponse)
Expand Down
16 changes: 13 additions & 3 deletions internal/oauth2/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,14 @@ type ClientConfig struct {
Password string
RefreshToken string
Assertion string `validate:"omitempty,json"`
AssertionJWT string `validate:"omitempty,jwt"`
SigningKey string `validate:"omitempty,uri|file"`
EncryptionKey string `validate:"omitempty,uri|file"`
RequestedTokenType string `validate:"omitempty,uri"`
SubjectToken string
SubjectTokenType string `validate:"omitempty,oneof=urn:ietf:params:oauth:token-type:access_token"`
SubjectTokenType string `validate:"omitempty,oneof=urn:ietf:params:oauth:token-type:access_token urn:ietf:params:oauth:token-type:id_token"`
ActorToken string
ActorTokenType string `validate:"omitempty,oneof=urn:ietf:params:oauth:token-type:access_token"`
ActorTokenType string `validate:"omitempty,oneof=urn:ietf:params:oauth:token-type:access_token urn:ietf:params:oauth:token-type:id_token"`
IDTokenHint string
LoginHint string
IDPHint string
Expand Down Expand Up @@ -531,7 +533,11 @@ func RequestToken(
case JWTBearerGrantType:
var assertion string

if assertion, request.SigningKey, err = SignJWT(
// A grant minted elsewhere - an ID-JAG, say - is presented as-is: signing our own claims
// over it would replace the very assertion the server is meant to verify.
if cconfig.AssertionJWT != "" {
assertion = cconfig.AssertionJWT
} else if assertion, request.SigningKey, err = SignJWT(
AssertionClaims(sconfig, cconfig),
JWKSigner(cconfig.SigningKey, hc),
); err != nil {
Expand All @@ -543,6 +549,10 @@ func RequestToken(
request.Form.Set("subject_token", cconfig.SubjectToken)
request.Form.Set("subject_token_type", cconfig.SubjectTokenType)

if cconfig.RequestedTokenType != "" {
request.Form.Set("requested_token_type", cconfig.RequestedTokenType)
}

if cconfig.ActorToken != "" {
request.Form.Set("actor_token", cconfig.ActorToken)
request.Form.Set("actor_token_type", cconfig.ActorTokenType)
Expand Down
11 changes: 7 additions & 4 deletions internal/oauth2/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ type Request struct {
JARM map[string]interface{}
RequestObject string
SigningKey interface{}
EncryptionKey interface{}
Cert *x509.Certificate
// The client assertion is signed independently of the grant assertion and the request
// object, so its key must not share a field with them.
ClientAssertionKey interface{}
EncryptionKey interface{}
Cert *x509.Certificate
}

func (r *Request) AuthorizeRequest(
Expand Down Expand Up @@ -205,7 +208,7 @@ func (r *Request) AuthenticateClient(
case ClientSecretJwtAuthMethod:
var clientAssertion string

if clientAssertion, r.SigningKey, err = SignJWT(
if clientAssertion, r.ClientAssertionKey, err = SignJWT(
ClientAssertionClaims(sconfig, cconfig),
SecretSigner([]byte(cconfig.ClientSecret)),
); err != nil {
Expand All @@ -217,7 +220,7 @@ func (r *Request) AuthenticateClient(
case PrivateKeyJwtAuthMethod:
var clientAssertion string

if clientAssertion, r.SigningKey, err = SignJWT(
if clientAssertion, r.ClientAssertionKey, err = SignJWT(
ClientAssertionClaims(sconfig, cconfig),
JWKSigner(cconfig.SigningKey, hc),
); err != nil {
Expand Down
154 changes: 140 additions & 14 deletions internal/oauth2/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,7 @@ func TestRequestTokenResource(t *testing.T) {

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
var got url.Values

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)

got, err = url.ParseQuery(string(body))
require.NoError(t, err)

w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"access_token":"tok","token_type":"Bearer","expires_in":3600}`))
}))
defer srv.Close()
srv, form := formCaptureServer(t)

cconfig := oauth2.ClientConfig{
ClientID: "test-client",
Expand All @@ -96,7 +84,145 @@ func TestRequestTokenResource(t *testing.T) {
_, _, err := oauth2.RequestToken(context.Background(), cconfig, sconfig, &http.Client{})
require.NoError(t, err)

require.Equal(t, tc.expected, got["resource"])
require.Equal(t, tc.expected, form()["resource"])
})
}
}

func TestRequestTokenRequestedTokenType(t *testing.T) {
tests := map[string]struct {
requestedTokenType string
expected []string
}{
"none": {
requestedTokenType: "",
expected: nil,
},
"id-jag": {
requestedTokenType: "urn:ietf:params:oauth:token-type:id-jag",
expected: []string{"urn:ietf:params:oauth:token-type:id-jag"},
},
"access token": {
requestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
expected: []string{"urn:ietf:params:oauth:token-type:access_token"},
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
srv, form := formCaptureServer(t)

cconfig := oauth2.ClientConfig{
ClientID: "test-client",
ClientSecret: "test-secret",
GrantType: oauth2.TokenExchangeGrantType,
AuthMethod: oauth2.ClientSecretPostAuthMethod,
SubjectToken: "subject-token",
SubjectTokenType: "urn:ietf:params:oauth:token-type:access_token",
RequestedTokenType: tc.requestedTokenType,
}
sconfig := oauth2.ServerConfig{TokenEndpoint: srv.URL}

_, _, err := oauth2.RequestToken(context.Background(), cconfig, sconfig, &http.Client{})
require.NoError(t, err)

require.Equal(t, tc.expected, form()["requested_token_type"])
})
}
}

// An ID-JAG is signed by the identity provider, so the redeeming client presents the token it was
// given rather than signing one of its own.
func TestRequestTokenAssertionJWT(t *testing.T) {
const grant = "eyJ0eXAiOiJvYXV0aC1pZC1qYWcrand0IiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2lkcC5leGFtcGxlLmNvbSJ9.signature"

srv, form := formCaptureServer(t)

cconfig := oauth2.ClientConfig{
ClientID: "test-client",
ClientSecret: "test-secret",
GrantType: oauth2.JWTBearerGrantType,
AuthMethod: oauth2.ClientSecretPostAuthMethod,
AssertionJWT: grant,
}
sconfig := oauth2.ServerConfig{TokenEndpoint: srv.URL}

request, _, err := oauth2.RequestToken(context.Background(), cconfig, sconfig, &http.Client{})
require.NoError(t, err)

require.Equal(t, []string{grant}, form()["assertion"])
require.Nil(t, request.SigningKey)
}

func TestRequestTokenAssertionJWTUnsetStillSigns(t *testing.T) {
Comment thread
mschaab-SA marked this conversation as resolved.
srv, form := formCaptureServer(t)

cconfig := oauth2.ClientConfig{
ClientID: "test-client",
ClientSecret: "test-secret",
GrantType: oauth2.JWTBearerGrantType,
AuthMethod: oauth2.ClientSecretPostAuthMethod,
SigningKey: "../../data/rsa/key.json",
Assertion: `{"sub":"jdoe@example.com"}`,
}
sconfig := oauth2.ServerConfig{TokenEndpoint: srv.URL}

request, _, err := oauth2.RequestToken(context.Background(), cconfig, sconfig, &http.Client{})
require.NoError(t, err)

assertion := form().Get("assertion")
require.NotEmpty(t, assertion)

token, claims, err := oauth2.UnsafeParseJWT(assertion)
require.NoError(t, err)
require.Equal(t, "RS256", token.Headers[0].Algorithm)
require.Equal(t, "jdoe@example.com", claims["sub"])
require.NotNil(t, request.SigningKey)
}

// Client authentication signs its own assertion, so its key must not be mistaken for one standing
// behind a pre-signed grant.
func TestRequestTokenAssertionJWTKeepsClientAuthKeySeparate(t *testing.T) {
const grant = "eyJ0eXAiOiJvYXV0aC1pZC1qYWcrand0IiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2lkcC5leGFtcGxlLmNvbSJ9.signature"

srv, form := formCaptureServer(t)

cconfig := oauth2.ClientConfig{
ClientID: "test-client",
GrantType: oauth2.JWTBearerGrantType,
AuthMethod: oauth2.PrivateKeyJwtAuthMethod,
SigningKey: "../../data/rsa/key.json",
AssertionJWT: grant,
}
sconfig := oauth2.ServerConfig{TokenEndpoint: srv.URL}

request, _, err := oauth2.RequestToken(context.Background(), cconfig, sconfig, &http.Client{})
require.NoError(t, err)

require.Equal(t, []string{grant}, form()["assertion"])
require.NotEmpty(t, form().Get("client_assertion"))

require.Nil(t, request.SigningKey)
require.NotNil(t, request.ClientAssertionKey)
}

func formCaptureServer(t *testing.T) (*httptest.Server, func() url.Values) {
t.Helper()

var got url.Values

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
require.NoError(t, err)

got, err = url.ParseQuery(string(body))
require.NoError(t, err)

w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"access_token":"tok","token_type":"Bearer","expires_in":3600}`))
}))

t.Cleanup(srv.Close)

return srv, func() url.Values { return got }
}