mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-02 11:31:57 +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:
@@ -71,6 +71,9 @@ func New(db *sql.DB, cfg lookup.Config, opts Options) (*lookup.Provider, error)
|
||||
OAuthUser: &oauthUserRouter{c: c,
|
||||
proc: procedure.NewOAuthUsers(run, res.Procs),
|
||||
direct: direct.NewOAuthUsers(base)},
|
||||
OAuthGrant: &oauthGrantRouter{c: c,
|
||||
proc: procedure.NewOAuthGrants(run, res.Procs),
|
||||
direct: direct.NewOAuthGrants(base)},
|
||||
Passkey: &passkeyRouter{c: c, proc: p, direct: direct.NewPasskey(base)},
|
||||
TOTP: &totpRouter{c: c,
|
||||
proc: procedure.NewTOTP(run, res.Procs),
|
||||
@@ -90,6 +93,7 @@ func Failed(err error) *lookup.Provider {
|
||||
Keys: &keysRouter{c: c},
|
||||
OAuthClient: &oauthClientRouter{c: c},
|
||||
OAuthUser: &oauthUserRouter{c: c},
|
||||
OAuthGrant: &oauthGrantRouter{c: c},
|
||||
Passkey: &passkeyRouter{c: c},
|
||||
TOTP: &totpRouter{c: c},
|
||||
Policy: &policyRouter{c: c},
|
||||
|
||||
@@ -113,6 +113,11 @@ func cleanup(t *testing.T, db *sql.DB, d dialect.Dialect, prefix string) {
|
||||
like := prefix + "%"
|
||||
for _, q := range []struct{ table, col string }{
|
||||
{"oauth_codes", "code"},
|
||||
{"oauth_consents", "client_id"},
|
||||
{"oauth_refresh_tokens", "client_id"},
|
||||
{"oauth_device_codes", "client_id"},
|
||||
{"oauth_par_requests", "client_id"},
|
||||
{"oauth_jti", "jti_key"},
|
||||
{"oauth_clients", "client_id"},
|
||||
{"token_blacklist", "token"},
|
||||
{"sec_column_rules", "schema_name"},
|
||||
|
||||
@@ -154,3 +154,27 @@ func TestConformanceMSSQLContainer(t *testing.T) {
|
||||
}
|
||||
runOnServer(t, "sqlserver", dsn("cf_direct"), "mssql", lookup.Config{}, true)
|
||||
}
|
||||
|
||||
// TestContainerLifecycle checks the start/stop plumbing the container tests rely on: the
|
||||
// container comes up and accepts connections, and after stop it is gone (it runs with --rm).
|
||||
func TestContainerLifecycle(t *testing.T) {
|
||||
rt := containerRuntime(t)
|
||||
port := startContainer(t, rt, "docker.io/library/postgres:16-alpine", "5432", map[string]string{"POSTGRES_PASSWORD": containerPassword})
|
||||
waitReady(t, "pgx", fmt.Sprintf("postgres://postgres:%s@127.0.0.1:%s/postgres?sslmode=disable", containerPassword, port), 90*time.Second)
|
||||
|
||||
listed := func() string {
|
||||
return run(t, 30*time.Second, rt, "ps", "-q", "--filter", "ancestor=docker.io/library/postgres:16-alpine")
|
||||
}
|
||||
id := listed()
|
||||
if id == "" {
|
||||
t.Fatal("container is not running after start")
|
||||
}
|
||||
run(t, time.Minute, rt, "stop", "-t", "2", id)
|
||||
deadline := time.Now().Add(30 * time.Second)
|
||||
for listed() != "" {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("container still present after stop")
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +199,22 @@ func (r *oauthClientRouter) Revoke(ctx context.Context, token string) error {
|
||||
return st.Revoke(ctx, token)
|
||||
}
|
||||
|
||||
func (r *oauthClientRouter) UpdateClient(ctx context.Context, client *sectypes.OAuthServerClient) error {
|
||||
st, err := pick[lookup.OAuthClientStore](r.c, ctx, lookup.OpOAuthUpdateClient, r.c.procs.OAuthUpdateClient, r.proc, r.direct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.UpdateClient(ctx, client)
|
||||
}
|
||||
|
||||
func (r *oauthClientRouter) DeleteClient(ctx context.Context, clientID string) error {
|
||||
st, err := pick[lookup.OAuthClientStore](r.c, ctx, lookup.OpOAuthDeleteClient, r.c.procs.OAuthDeleteClient, r.proc, r.direct)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.DeleteClient(ctx, clientID)
|
||||
}
|
||||
|
||||
type oauthUserRouter struct {
|
||||
c *chooser
|
||||
proc, direct lookup.OAuthUserStore
|
||||
@@ -394,3 +410,134 @@ func (r *policyRouter) RowSecurity(ctx context.Context, userRef any, schema, tab
|
||||
}
|
||||
return st.RowSecurity(ctx, userRef, schema, table)
|
||||
}
|
||||
|
||||
type oauthGrantRouter struct {
|
||||
c *chooser
|
||||
proc, direct lookup.OAuthGrantStore
|
||||
}
|
||||
|
||||
var _ lookup.OAuthGrantStore = (*oauthGrantRouter)(nil)
|
||||
|
||||
func (r *oauthGrantRouter) pick(ctx context.Context, op lookup.Op, proc string) (lookup.OAuthGrantStore, error) {
|
||||
return pick[lookup.OAuthGrantStore](r.c, ctx, op, proc, r.proc, r.direct)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) SaveConsent(ctx context.Context, c lookup.Consent) error {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthSaveConsent, r.c.procs.OAuthSaveConsent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.SaveConsent(ctx, c)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) GetConsent(ctx context.Context, userID int, clientID string) (*lookup.Consent, error) {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthGetConsent, r.c.procs.OAuthGetConsent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st.GetConsent(ctx, userID, clientID)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) RevokeConsent(ctx context.Context, userID int, clientID string) error {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthRevokeConsent, r.c.procs.OAuthRevokeConsent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.RevokeConsent(ctx, userID, clientID)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) SaveRefresh(ctx context.Context, t lookup.RefreshToken) error {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthSaveRefresh, r.c.procs.OAuthSaveRefresh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.SaveRefresh(ctx, t)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) RotateRefresh(ctx context.Context, oldHash string, next lookup.RefreshToken) (*lookup.RefreshToken, error) {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthRotateRefresh, r.c.procs.OAuthRotateRefresh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st.RotateRefresh(ctx, oldHash, next)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) PeekRefresh(ctx context.Context, hash string) (*lookup.RefreshToken, error) {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthPeekRefresh, r.c.procs.OAuthPeekRefresh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st.PeekRefresh(ctx, hash)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) RevokeRefreshFamily(ctx context.Context, familyID string) error {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthRevokeRefreshFamily, r.c.procs.OAuthRevokeRefreshFamily)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.RevokeRefreshFamily(ctx, familyID)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) RevokeRefreshBySession(ctx context.Context, sessionToken string) error {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthRevokeRefreshByUser, r.c.procs.OAuthRevokeRefreshByUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.RevokeRefreshBySession(ctx, sessionToken)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) CreateDevice(ctx context.Context, d lookup.DeviceCode) error {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthCreateDevice, r.c.procs.OAuthCreateDevice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.CreateDevice(ctx, d)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) DeviceByUserCode(ctx context.Context, userCode string) (*lookup.DeviceCode, error) {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthDeviceByUserCode, r.c.procs.OAuthDeviceByUserCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st.DeviceByUserCode(ctx, userCode)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) DeviceDecide(ctx context.Context, userCode string, approve bool, userID int, sessionToken string) error {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthDeviceDecide, r.c.procs.OAuthDeviceDecide)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.DeviceDecide(ctx, userCode, approve, userID, sessionToken)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) DevicePoll(ctx context.Context, deviceHash string) (*lookup.DeviceCode, error) {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthDevicePoll, r.c.procs.OAuthDevicePoll)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st.DevicePoll(ctx, deviceHash)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) SavePushedRequest(ctx context.Context, req lookup.PushedRequest) error {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthSavePAR, r.c.procs.OAuthSavePAR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return st.SavePushedRequest(ctx, req)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) ConsumePushedRequest(ctx context.Context, requestURI string) (*lookup.PushedRequest, error) {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthConsumePAR, r.c.procs.OAuthConsumePAR)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st.ConsumePushedRequest(ctx, requestURI)
|
||||
}
|
||||
|
||||
func (r *oauthGrantRouter) SeenJTI(ctx context.Context, key string, expires time.Time) (bool, error) {
|
||||
st, err := r.pick(ctx, lookup.OpOAuthSeenJTI, r.c.procs.OAuthSeenJTI)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return st.SeenJTI(ctx, key, expires)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user