Skip to content

Commit 61632f8

Browse files
authored
feat(oauthserver): use NewOAuthServerAuthorizationParams & configurable ttl for authorization (#2254)
## Summary This PR adds support for configurable OAuth authorization TTL (Time To Live) using the`GOTRUE_OAUTH_SERVER_AUTHORIZATION_TTL` environment variable, replacing the previous hardcoded 10-minute expiration. This is safe & requires no migration as it's not rolled out yet! Also introduces params struct to be used with `NewOAuthServerAuthorization`.
1 parent 162788f commit 61632f8

File tree

4 files changed

+81
-64
lines changed

4 files changed

+81
-64
lines changed

internal/api/oauthserver/authorize.go

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,17 @@ func (s *Server) OAuthServerAuthorize(w http.ResponseWriter, r *http.Request) er
136136
}
137137

138138
// Store authorization request in database (without user initially)
139-
authorization := models.NewOAuthServerAuthorization(
140-
client.ID, // Use the client's UUID instead of the public client_id string
141-
params.RedirectURI,
142-
params.Scope,
143-
params.State,
144-
params.Resource,
145-
params.CodeChallenge,
146-
params.CodeChallengeMethod,
147-
params.Nonce,
148-
)
139+
authorization := models.NewOAuthServerAuthorization(models.NewOAuthServerAuthorizationParams{
140+
ClientID: client.ID,
141+
RedirectURI: params.RedirectURI,
142+
Scope: params.Scope,
143+
State: params.State,
144+
Resource: params.Resource,
145+
CodeChallenge: params.CodeChallenge,
146+
CodeChallengeMethod: params.CodeChallengeMethod,
147+
TTL: config.OAuthServer.AuthorizationTTL,
148+
Nonce: params.Nonce,
149+
})
149150

150151
if err := models.CreateOAuthServerAuthorization(db, authorization); err != nil {
151152
// Error creating authorization - redirect with server_error

internal/conf/configuration.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ type OAuthServerConfiguration struct {
7676
Enabled bool `json:"enabled" default:"false"`
7777
AllowDynamicRegistration bool `json:"allow_dynamic_registration" split_words:"true"`
7878
AuthorizationPath string `json:"authorization_path" split_words:"true"`
79-
AuthorizationTimeout time.Duration `json:"authorization_timeout" split_words:"true" default:"5m"`
79+
AuthorizationTTL time.Duration `json:"authorization_ttl" split_words:"true" default:"10m"`
8080
// Placeholder for now, for (near) future extensibility
8181
DefaultScope string `json:"default_scope" split_words:"true" default:"email"`
8282
}

internal/models/oauth_authorization.go

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,44 +67,57 @@ func (OAuthServerAuthorization) TableName() string {
6767
return "oauth_authorizations"
6868
}
6969

70+
// NewOAuthServerAuthorizationParams contains parameters for creating a new OAuth server authorization
71+
type NewOAuthServerAuthorizationParams struct {
72+
ClientID uuid.UUID
73+
RedirectURI string
74+
Scope string
75+
State string
76+
Resource string
77+
CodeChallenge string
78+
CodeChallengeMethod string
79+
TTL time.Duration
80+
Nonce string
81+
}
82+
7083
// NewOAuthServerAuthorization creates a new OAuth server authorization request without user (for initial flow)
71-
func NewOAuthServerAuthorization(clientID uuid.UUID, redirectURI, scope, state, resource, codeChallenge, codeChallengeMethod, nonce string) *OAuthServerAuthorization {
84+
func NewOAuthServerAuthorization(params NewOAuthServerAuthorizationParams) *OAuthServerAuthorization {
7285
id := uuid.Must(uuid.NewV4())
7386
authorizationID := crypto.SecureAlphanumeric(32) // Generate random ID for frontend
7487

7588
now := time.Now()
76-
expiresAt := now.Add(10 * time.Minute) // 10 minute expiration
89+
expiresAt := now.Add(params.TTL)
7790

7891
auth := &OAuthServerAuthorization{
7992
ID: id,
8093
AuthorizationID: authorizationID,
81-
ClientID: clientID,
94+
ClientID: params.ClientID,
8295
UserID: nil, // No user yet
83-
RedirectURI: redirectURI,
84-
Scope: scope,
96+
RedirectURI: params.RedirectURI,
97+
Scope: params.Scope,
8598
ResponseType: OAuthServerResponseTypeCode,
8699
Status: OAuthServerAuthorizationPending,
87100
CreatedAt: now,
88101
ExpiresAt: expiresAt,
89102
}
90103

91-
if state != "" {
92-
auth.State = &state
104+
if params.State != "" {
105+
auth.State = &params.State
93106
}
94-
if resource != "" {
95-
auth.Resource = &resource
107+
if params.Resource != "" {
108+
auth.Resource = &params.Resource
96109
}
97-
if codeChallenge != "" {
98-
auth.CodeChallenge = &codeChallenge
110+
if params.CodeChallenge != "" {
111+
auth.CodeChallenge = &params.CodeChallenge
99112
}
100-
if codeChallengeMethod != "" {
113+
if params.CodeChallengeMethod != "" {
101114
// Normalize code challenge method to lowercase for database storage
102115
// Database enum expects 's256' and 'plain' (lowercase)
103-
normalizedMethod := strings.ToLower(codeChallengeMethod)
116+
normalizedMethod := strings.ToLower(params.CodeChallengeMethod)
104117
auth.CodeChallengeMethod = &normalizedMethod
105118
}
106-
if nonce != "" {
107-
auth.Nonce = &nonce
119+
if params.Nonce != "" {
120+
auth.Nonce = &params.Nonce
108121
}
109122

110123
return auth

internal/models/oauth_authorization_test.go

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,27 @@ import (
1111

1212
func TestNewOAuthServerAuthorization(t *testing.T) {
1313
clientID := uuid.Must(uuid.NewV4())
14-
redirectURI := "https://example.com/callback"
15-
scope := "openid profile"
16-
state := "random-state"
17-
codeChallenge := "test-challenge"
18-
codeChallengeMethod := "S256"
19-
resource := "https://api.example.com/"
2014

21-
auth := NewOAuthServerAuthorization(clientID, redirectURI, scope, state, resource, codeChallenge, codeChallengeMethod, "")
15+
auth := NewOAuthServerAuthorization(NewOAuthServerAuthorizationParams{
16+
ClientID: clientID,
17+
RedirectURI: "https://example.com/callback",
18+
Scope: "openid profile",
19+
State: "random-state",
20+
Resource: "https://api.example.com/",
21+
CodeChallenge: "test-challenge",
22+
CodeChallengeMethod: "S256",
23+
TTL: 10 * time.Minute,
24+
})
2225

2326
assert.NotEmpty(t, auth.ID)
2427
assert.NotEmpty(t, auth.AuthorizationID)
2528
assert.Equal(t, clientID, auth.ClientID)
2629
assert.Nil(t, auth.UserID)
27-
assert.Equal(t, redirectURI, auth.RedirectURI)
28-
assert.Equal(t, scope, auth.Scope)
29-
assert.Equal(t, state, *auth.State)
30-
assert.Equal(t, resource, *auth.Resource)
31-
assert.Equal(t, codeChallenge, *auth.CodeChallenge)
30+
assert.Equal(t, "https://example.com/callback", auth.RedirectURI)
31+
assert.Equal(t, "openid profile", auth.Scope)
32+
assert.Equal(t, "random-state", *auth.State)
33+
assert.Equal(t, "https://api.example.com/", *auth.Resource)
34+
assert.Equal(t, "test-challenge", *auth.CodeChallenge)
3235
assert.Equal(t, "s256", *auth.CodeChallengeMethod) // Should be normalized to lowercase
3336
assert.Equal(t, OAuthServerResponseTypeCode, auth.ResponseType)
3437
assert.Equal(t, OAuthServerAuthorizationPending, auth.Status)
@@ -52,16 +55,15 @@ func TestNewOAuthServerAuthorization_CodeChallengeMethodNormalization(t *testing
5255

5356
for _, tc := range testCases {
5457
t.Run(tc.name, func(t *testing.T) {
55-
auth := NewOAuthServerAuthorization(
56-
uuid.Must(uuid.NewV4()),
57-
"https://example.com/callback",
58-
"openid",
59-
"state",
60-
"",
61-
"challenge",
62-
tc.input,
63-
"", // nonce
64-
)
58+
auth := NewOAuthServerAuthorization(NewOAuthServerAuthorizationParams{
59+
ClientID: uuid.Must(uuid.NewV4()),
60+
RedirectURI: "https://example.com/callback",
61+
Scope: "openid",
62+
State: "state",
63+
CodeChallenge: "challenge",
64+
CodeChallengeMethod: tc.input,
65+
TTL: 10 * time.Minute,
66+
})
6567

6668
assert.Equal(t, tc.expected, *auth.CodeChallengeMethod,
6769
"Expected code_challenge_method to be normalized to %s, got %s", tc.expected, *auth.CodeChallengeMethod)
@@ -75,29 +77,30 @@ func TestNewOAuthServerAuthorization_WithNonce(t *testing.T) {
7577

7678
// Test with nonce
7779
authWithNonce := NewOAuthServerAuthorization(
78-
clientID,
79-
"https://example.com/callback",
80-
"openid",
81-
"state",
82-
"",
83-
"challenge",
84-
"S256",
85-
nonce,
80+
NewOAuthServerAuthorizationParams{
81+
ClientID: clientID,
82+
RedirectURI: "https://example.com/callback",
83+
Scope: "openid",
84+
State: "state",
85+
CodeChallenge: "challenge",
86+
CodeChallengeMethod: "S256",
87+
Nonce: nonce,
88+
},
8689
)
8790

8891
assert.NotNil(t, authWithNonce.Nonce)
8992
assert.Equal(t, nonce, *authWithNonce.Nonce)
9093

9194
// Test without nonce (empty string)
9295
authWithoutNonce := NewOAuthServerAuthorization(
93-
clientID,
94-
"https://example.com/callback",
95-
"openid",
96-
"state",
97-
"",
98-
"challenge",
99-
"S256",
100-
"",
96+
NewOAuthServerAuthorizationParams{
97+
ClientID: clientID,
98+
RedirectURI: "https://example.com/callback",
99+
Scope: "openid",
100+
State: "state",
101+
CodeChallenge: "challenge",
102+
CodeChallengeMethod: "S256",
103+
},
101104
)
102105

103106
assert.Nil(t, authWithoutNonce.Nonce)

0 commit comments

Comments
 (0)