Skip to content

Add ReadKeys and RotateKeys methods and extend ConnectionOptions with TokenEndpointAuthMethod and TokenEndpointAuthSigningAlg in ConnectionManager #559

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
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
58 changes: 58 additions & 0 deletions management/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,34 @@ type Connection struct {
ShowAsButton *bool `json:"show_as_button,omitempty"`
}

// ConnectionKey is used to fetch public keys for a connection.
type ConnectionKey struct {
// The key ID of the signing key.
KID *string `json:"kid,omitempty"`
// The public certificate of the signing key.
Cert *string `json:"cert,omitempty"`
// The public certificate of the signing key in PKCS7 format.
PKCS *string `json:"pkcs,omitempty"`
// True if the key is the current key.
Current *bool `json:"current,omitempty"`
// True if the key is the next key.
Next *bool `json:"next,omitempty"`
// True if the key is the previous key.
Previous *bool `json:"previous,omitempty"`
// The date and time when the key became the current key.
CurrentSince *string `json:"current_since,omitempty"`
// The certificate fingerprint.
Fingerprint *string `json:"fingerprint,omitempty"`
// The certificate thumbprint.
Thumbprint *string `json:"thumbprint,omitempty"`
// The signing key algorithm.
Algorithm *string `json:"algorithm,omitempty"`
// The signing key use, whether for encryption or signing.
KeyUse *string `json:"key_use,omitempty"`
// The subject distinguished name (DN) of the certificate.
SubjectDN *string `json:"subject_dn,omitempty"`
}

// ConnectionEnabledClientList is a list of enabled clients for a connection.
type ConnectionEnabledClientList struct {
List
Expand Down Expand Up @@ -600,6 +628,12 @@ type ConnectionOptionsOkta struct {

ConnectionSettings *ConnectionOptionsOIDCConnectionSettings `json:"connection_settings,omitempty"`
AttributeMap *ConnectionOptionsOIDCAttributeMap `json:"attribute_map,omitempty"`

// TokenEndpointAuthMethod specifies the authentication method for the token endpoint.
TokenEndpointAuthMethod *string `json:"token_endpoint_auth_method,omitempty"`

// TokenEndpointAuthSigningAlg specifies the signing algorithm for the token endpoint.
TokenEndpointAuthSigningAlg *string `json:"token_endpoint_auth_signing_alg,omitempty"`
}

// Scopes returns the scopes for ConnectionOptionsOkta.
Expand Down Expand Up @@ -1068,6 +1102,12 @@ type ConnectionOptionsOIDC struct {

ConnectionSettings *ConnectionOptionsOIDCConnectionSettings `json:"connection_settings,omitempty"`
AttributeMap *ConnectionOptionsOIDCAttributeMap `json:"attribute_map,omitempty"`

// TokenEndpointAuthMethod specifies the authentication method for the token endpoint.
TokenEndpointAuthMethod *string `json:"token_endpoint_auth_method,omitempty"`

// TokenEndpointAuthSigningAlg specifies the signing algorithm for the token endpoint.
TokenEndpointAuthSigningAlg *string `json:"token_endpoint_auth_signing_alg,omitempty"`
}

// ConnectionOptionsOIDCConnectionSettings contains PKCE configuration for the connection.
Expand Down Expand Up @@ -1689,3 +1729,21 @@ func (m *ConnectionManager) DeleteSCIMToken(ctx context.Context, id, tokenID str
err = m.management.Request(ctx, "DELETE", m.management.URI("connections", id, "scim-configuration", "tokens", tokenID), nil, opts...)
return
}

// ReadKeys returns the set of the connection’s public keys used to verify JWT signatures.
// This method only works with enterprise connections.
//
// See: https://auth0.com/docs/api/management/v2/connections/get-connection-keys
func (m *ConnectionManager) ReadKeys(ctx context.Context, id string, opts ...RequestOption) (keys []*ConnectionKey, err error) {
err = m.management.Request(ctx, "GET", m.management.URI("connections", id, "keys"), &keys, opts...)
return
}

// RotateKeys rotates the connection's public key used to verify signatures on signed JWTs.
// This method only works with enterprise connections.
//
// See: https://auth0.com/docs/api/management/v2/connections/rotate-connection-keys
func (m *ConnectionManager) RotateKeys(ctx context.Context, id string, opts ...RequestOption) (key *ConnectionKey, err error) {
err = m.management.Request(ctx, "POST", m.management.URI("connections", id, "keys", "rotate"), &key, opts...)
return
}
125 changes: 125 additions & 0 deletions management/connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"net/http"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -1222,6 +1223,130 @@ func TestConnectionManager_DeleteSCIMToken(t *testing.T) {
cleanupSCIMConfig(t, expectedConnection.GetID())
})
}

func TestConnectionManager_ReadKeys(t *testing.T) {
configureHTTPTestRecordings(t)

tests := []struct {
name string
strategy string
options interface{}
}{
{
name: "OIDC Connection",
strategy: "oidc",
options: &ConnectionOptionsOIDC{
ClientID: auth0.String("4ef8d976-71bd-4473-a7ce-087d3f0fafd8"),
Scope: auth0.String("openid"),
Issuer: auth0.String("https://example.com"),
AuthorizationEndpoint: auth0.String("https://example.com"),
JWKSURI: auth0.String("https://example.com/jwks"),
Type: auth0.String("front_channel"),
DiscoveryURL: auth0.String("https://www.paypalobjects.com/.well-known/openid-configuration"),
UpstreamParams: map[string]interface{}{
"screen_name": map[string]interface{}{
"alias": "login_hint",
},
},
TokenEndpointAuthMethod: auth0.String("private_key_jwt"),
TokenEndpointAuthSigningAlg: auth0.String("RS256"),
},
},
{
name: "Okta Connection",
strategy: "okta",
options: &ConnectionOptionsOkta{
ClientID: auth0.String("4ef8d976-71bd-4473-a7ce-087d3f0fafd8"),
ClientSecret: auth0.String("mySecret"),
Scope: auth0.String("openid"),
Domain: auth0.String("domain.okta.com"),
Issuer: auth0.String("https://example.com"),
AuthorizationEndpoint: auth0.String("https://example.com"),
JWKSURI: auth0.String("https://example.com/jwks"),
UpstreamParams: map[string]interface{}{
"screen_name": map[string]interface{}{
"alias": "login_hint",
},
},
TokenEndpointAuthMethod: auth0.String("private_key_jwt"),
TokenEndpointAuthSigningAlg: auth0.String("RS256"),
},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
conn := givenAConnection(t, connectionTestCase{
connection: Connection{
Name: auth0.Stringf("Test-%s-Connection-%d", strings.ToUpper(tc.strategy), time.Now().Unix()),
Strategy: auth0.String(tc.strategy),
},
options: tc.options,
})

keys, err := api.Connection.ReadKeys(context.Background(), conn.GetID())
assert.NoError(t, err)
assert.NotEmpty(t, keys)
})
}
}

func TestConnectionManager_RotateKeys(t *testing.T) {
configureHTTPTestRecordings(t)

conn := givenAConnection(t, connectionTestCase{
connection: Connection{
Name: auth0.Stringf("Test-Connection-Rotate-Keys-%d", time.Now().Unix()),
Strategy: auth0.String("oidc"),
},
options: &ConnectionOptionsOIDC{
ClientID: auth0.String("4ef8d976-71bd-4473-a7ce-087d3f0fafd8"),
Scope: auth0.String("openid"),
Issuer: auth0.String("https://example.com"),
AuthorizationEndpoint: auth0.String("https://example.com"),
JWKSURI: auth0.String("https://example.com/jwks"),
Type: auth0.String("front_channel"),
DiscoveryURL: auth0.String("https://www.paypalobjects.com/.well-known/openid-configuration"),
UpstreamParams: map[string]interface{}{
"screen_name": map[string]interface{}{
"alias": "login_hint",
},
},
TokenEndpointAuthMethod: auth0.String("private_key_jwt"),
TokenEndpointAuthSigningAlg: auth0.String("RS256"),
},
})

ctx := context.Background()

beforeKeys, err := api.Connection.ReadKeys(ctx, conn.GetID())
assert.NoError(t, err)
assert.NotEmpty(t, beforeKeys)

rotatedKey, err := api.Connection.RotateKeys(ctx, conn.GetID())
assert.NoError(t, err)
assert.NotNil(t, rotatedKey)

afterKeys, err := api.Connection.ReadKeys(ctx, conn.GetID())
assert.NoError(t, err)
assert.NotEmpty(t, afterKeys)

foundRotated := false
for _, k := range afterKeys {
if k.GetKID() == rotatedKey.GetKID() {
foundRotated = true
break
}
}
assert.True(t, foundRotated, "Rotated key should be present in afterKeys")

assert.NotEqual(t, len(beforeKeys), len(afterKeys), "Keys should be different after rotation")

t.Cleanup(func() {
cleanupConnection(t, conn.GetID())
})
}

func TestConnectionOptionsUsernameAttribute_MarshalJSON(t *testing.T) {
for attribute, expected := range map[*ConnectionOptionsUsernameAttribute]string{
{
Expand Down
133 changes: 133 additions & 0 deletions management/management.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading