mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-09 14:56:27 +00:00
feat(security): full OAuth 2.1 / OpenID Connect server and OIDC relying-party client
Authorization server: consent and scopes, OIDC (nonce, auth_time, acr, sid, at_hash, signed userinfo, RP-initiated and back-channel logout), managed refresh tokens with rotation and reuse detection, RFC 9068 JWT access tokens, DPoP, PAR, device grant, token exchange, private_key_jwt, RFC 7591/7592 registration, RFC 9207 iss, signing keyring with rotation. State is DB-backed through a new lookup.OAuthGrantStore (procedure and direct backends, four dialect DDLs, conformance cases). Client side: WithOIDC discovery, PKCE, nonce, id_token validation, OAuth2LogoutURL. PeekRefresh now returns already rotated tokens so RotateRefresh can detect reuse. Docs: OAUTH2_SERVER.md, oauth2_full_example.go, breaking_changes.md step 8.
This commit is contained in:
@@ -60,6 +60,10 @@ func (o *OAuthClients) RegisterClient(ctx context.Context, client *sectypes.OAut
|
||||
return nil, fmt.Errorf("failed to marshal allowed_scopes: %w", err)
|
||||
}
|
||||
|
||||
meta, err := client.ClientMetadataJSON()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal client metadata: %w", err)
|
||||
}
|
||||
err = o.do(func(q Querier) error {
|
||||
return o.Insert(lookup.EntityOAuthClients).Set(
|
||||
Set(lookup.OAuthClientsClientID, client.ClientID),
|
||||
@@ -70,33 +74,86 @@ func (o *OAuthClients) RegisterClient(ctx context.Context, client *sectypes.OAut
|
||||
Set(lookup.OAuthClientsClientSecretHash, nullIfEmpty(client.ClientSecretHash)),
|
||||
Set(lookup.OAuthClientsTokenEndpointAuthMethod, authMethod),
|
||||
Set(lookup.OAuthClientsIsActive, true),
|
||||
Set(lookup.OAuthClientsMetadata, nullIfEmpty(meta)),
|
||||
Set(lookup.OAuthClientsCreatedAt, o.Now()),
|
||||
).Exec(ctx, q)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to register client: %w", err)
|
||||
}
|
||||
return §ypes.OAuthServerClient{
|
||||
ClientID: client.ClientID,
|
||||
RedirectURIs: client.RedirectURIs,
|
||||
ClientName: client.ClientName,
|
||||
GrantTypes: grantTypes,
|
||||
AllowedScopes: allowedScopes,
|
||||
ClientSecretHash: client.ClientSecretHash,
|
||||
TokenEndpointAuthMethod: authMethod,
|
||||
}, nil
|
||||
res := *client
|
||||
res.GrantTypes = grantTypes
|
||||
res.AllowedScopes = allowedScopes
|
||||
res.TokenEndpointAuthMethod = authMethod
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// UpdateClient implements lookup.OAuthClientStore: it rewrites the mutable registration
|
||||
// fields of an existing client (RFC 7592 management).
|
||||
func (o *OAuthClients) UpdateClient(ctx context.Context, client *sectypes.OAuthServerClient) error {
|
||||
redirects, err := o.d.EncodeJSON(client.RedirectURIs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if redirects == nil {
|
||||
redirects = "[]"
|
||||
}
|
||||
grants, err := o.d.EncodeJSON(client.GrantTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scopes, err := o.d.EncodeJSON(client.AllowedScopes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
meta, err := client.ClientMetadataJSON()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var n int64
|
||||
err = o.do(func(q Querier) error {
|
||||
var err error
|
||||
n, err = o.Update(lookup.EntityOAuthClients).Set(
|
||||
Set(lookup.OAuthClientsRedirectURIs, redirects),
|
||||
Set(lookup.OAuthClientsClientName, client.ClientName),
|
||||
Set(lookup.OAuthClientsGrantTypes, grants),
|
||||
Set(lookup.OAuthClientsAllowedScopes, scopes),
|
||||
Set(lookup.OAuthClientsClientSecretHash, nullIfEmpty(client.ClientSecretHash)),
|
||||
Set(lookup.OAuthClientsTokenEndpointAuthMethod, client.TokenEndpointAuthMethod),
|
||||
Set(lookup.OAuthClientsMetadata, nullIfEmpty(meta)),
|
||||
).Where(Eq(lookup.OAuthClientsClientID, client.ClientID), Eq(lookup.OAuthClientsIsActive, true)).Exec(ctx, q)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update client: %w", err)
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("client not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteClient implements lookup.OAuthClientStore: the client is deactivated.
|
||||
func (o *OAuthClients) DeleteClient(ctx context.Context, clientID string) error {
|
||||
return o.do(func(q Querier) error {
|
||||
_, err := o.Update(lookup.EntityOAuthClients).Set(Set(lookup.OAuthClientsIsActive, false)).
|
||||
Where(Eq(lookup.OAuthClientsClientID, clientID)).Exec(ctx, q)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// GetClient implements lookup.OAuthClientStore.
|
||||
func (o *OAuthClients) GetClient(ctx context.Context, clientID string) (*sectypes.OAuthServerClient, error) {
|
||||
var redirects, grants, scopes any
|
||||
var name, secret, method sql.NullString
|
||||
var meta any
|
||||
err := o.do(func(q Querier) error {
|
||||
return o.From(lookup.EntityOAuthClients).
|
||||
Cols(lookup.OAuthClientsRedirectURIs, lookup.OAuthClientsClientName, lookup.OAuthClientsGrantTypes,
|
||||
lookup.OAuthClientsAllowedScopes, lookup.OAuthClientsClientSecretHash, lookup.OAuthClientsTokenEndpointAuthMethod).
|
||||
lookup.OAuthClientsAllowedScopes, lookup.OAuthClientsClientSecretHash, lookup.OAuthClientsTokenEndpointAuthMethod,
|
||||
lookup.OAuthClientsMetadata).
|
||||
Where(Eq(lookup.OAuthClientsClientID, clientID), Eq(lookup.OAuthClientsIsActive, true)).
|
||||
QueryRow(ctx, q, &redirects, &name, &grants, &scopes, &secret, &method)
|
||||
QueryRow(ctx, q, &redirects, &name, &grants, &scopes, &secret, &method, &meta)
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -104,12 +161,17 @@ func (o *OAuthClients) GetClient(ctx context.Context, clientID string) (*sectype
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get client: %w", err)
|
||||
}
|
||||
res := §ypes.OAuthServerClient{
|
||||
ClientID: clientID,
|
||||
ClientName: name.String,
|
||||
ClientSecretHash: secret.String,
|
||||
TokenEndpointAuthMethod: method.String,
|
||||
res := §ypes.OAuthServerClient{}
|
||||
switch v := meta.(type) {
|
||||
case []byte:
|
||||
_ = res.ApplyClientMetadata(string(v))
|
||||
case string:
|
||||
_ = res.ApplyClientMetadata(v)
|
||||
}
|
||||
res.ClientID = clientID
|
||||
res.ClientName = name.String
|
||||
res.ClientSecretHash = secret.String
|
||||
res.TokenEndpointAuthMethod = method.String
|
||||
_ = o.d.DecodeJSON(redirects, &res.RedirectURIs)
|
||||
_ = o.d.DecodeJSON(grants, &res.GrantTypes)
|
||||
_ = o.d.DecodeJSON(scopes, &res.AllowedScopes)
|
||||
@@ -126,6 +188,10 @@ func (o *OAuthClients) SaveCode(ctx context.Context, code *sectypes.OAuthCode) e
|
||||
if method == "" {
|
||||
method = "S256"
|
||||
}
|
||||
extra, err := code.CodeExtraJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal code extra: %w", err)
|
||||
}
|
||||
return o.do(func(q Querier) error {
|
||||
return o.Insert(lookup.EntityOAuthCodes).Set(
|
||||
Set(lookup.OAuthCodesCode, code.Code),
|
||||
@@ -138,6 +204,7 @@ func (o *OAuthClients) SaveCode(ctx context.Context, code *sectypes.OAuthCode) e
|
||||
Set(lookup.OAuthCodesRefreshToken, code.RefreshToken),
|
||||
Set(lookup.OAuthCodesScopes, scopes),
|
||||
Set(lookup.OAuthCodesExpiresAt, code.ExpiresAt),
|
||||
Set(lookup.OAuthCodesExtra, nullIfEmpty(extra)),
|
||||
Set(lookup.OAuthCodesCreatedAt, o.Now()),
|
||||
).Exec(ctx, q)
|
||||
})
|
||||
@@ -148,15 +215,15 @@ func (o *OAuthClients) SaveCode(ctx context.Context, code *sectypes.OAuthCode) e
|
||||
func (o *OAuthClients) ExchangeCode(ctx context.Context, code string) (*sectypes.OAuthCode, error) {
|
||||
var res sectypes.OAuthCode
|
||||
var state, refresh sql.NullString
|
||||
var scopes any
|
||||
var scopes, extra any
|
||||
err := o.tx(ctx, func(q Querier) error {
|
||||
err := o.From(lookup.EntityOAuthCodes).
|
||||
Cols(lookup.OAuthCodesClientID, lookup.OAuthCodesRedirectURI, lookup.OAuthCodesClientState,
|
||||
lookup.OAuthCodesCodeChallenge, lookup.OAuthCodesCodeChallengeMethod, lookup.OAuthCodesSessionToken,
|
||||
lookup.OAuthCodesRefreshToken, lookup.OAuthCodesScopes).
|
||||
lookup.OAuthCodesRefreshToken, lookup.OAuthCodesScopes, lookup.OAuthCodesExtra).
|
||||
Where(Eq(lookup.OAuthCodesCode, code), Gt(lookup.OAuthCodesExpiresAt, o.Now())).
|
||||
QueryRow(ctx, q, &res.ClientID, &res.RedirectURI, &state, &res.CodeChallenge, &res.CodeChallengeMethod,
|
||||
&res.SessionToken, &refresh, &scopes)
|
||||
&res.SessionToken, &refresh, &scopes, &extra)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -179,6 +246,12 @@ func (o *OAuthClients) ExchangeCode(ctx context.Context, code string) (*sectypes
|
||||
res.ClientState = state.String
|
||||
res.RefreshToken = refresh.String
|
||||
_ = o.d.DecodeJSON(scopes, &res.Scopes)
|
||||
switch v := extra.(type) {
|
||||
case []byte:
|
||||
_ = res.ApplyCodeExtra(string(v))
|
||||
case string:
|
||||
_ = res.ApplyCodeExtra(v)
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user