@@ -20,13 +20,15 @@ import (
20
20
"github.com/coder/coder/v2/coderd/coderdtest"
21
21
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
22
22
"github.com/coder/coder/v2/coderd/database"
23
+ "github.com/coder/coder/v2/coderd/database/dbauthz"
23
24
"github.com/coder/coder/v2/coderd/database/dbtestutil"
24
25
"github.com/coder/coder/v2/coderd/database/dbtime"
25
26
"github.com/coder/coder/v2/coderd/oauth2provider"
26
27
"github.com/coder/coder/v2/coderd/userpassword"
27
28
"github.com/coder/coder/v2/coderd/util/ptr"
28
29
"github.com/coder/coder/v2/codersdk"
29
30
"github.com/coder/coder/v2/testutil"
31
+ "github.com/coder/serpent"
30
32
)
31
33
32
34
func TestOAuth2ProviderApps (t * testing.T ) {
@@ -1184,6 +1186,71 @@ func TestOAuth2ProviderCrossResourceAudienceValidation(t *testing.T) {
1184
1186
// For now, this verifies the basic token flow works correctly
1185
1187
}
1186
1188
1189
+ // TestOAuth2RefreshExpiryOutlivesAccess verifies that refresh token expiry is
1190
+ // greater than the provisioned access token (API key) expiry per configuration.
1191
+ func TestOAuth2RefreshExpiryOutlivesAccess (t * testing.T ) {
1192
+ t .Parallel ()
1193
+
1194
+ // Set explicit lifetimes to make comparison deterministic.
1195
+ db , pubsub := dbtestutil .NewDB (t )
1196
+ dv := coderdtest .DeploymentValues (t , func (d * codersdk.DeploymentValues ) {
1197
+ d .Sessions .DefaultDuration = serpent .Duration (1 * time .Hour )
1198
+ d .Sessions .RefreshDefaultDuration = serpent .Duration (48 * time .Hour )
1199
+ })
1200
+ ownerClient := coderdtest .New (t , & coderdtest.Options {
1201
+ Database : db ,
1202
+ Pubsub : pubsub ,
1203
+ DeploymentValues : dv ,
1204
+ })
1205
+ _ = coderdtest .CreateFirstUser (t , ownerClient )
1206
+ ctx := testutil .Context (t , testutil .WaitLong )
1207
+
1208
+ // Create app and secret
1209
+ // Keep suffix short to satisfy name validation (<=32 chars, alnum + hyphens).
1210
+ apps := generateApps (ctx , t , ownerClient , "ref-exp" )
1211
+ //nolint:gocritic // Owner permission required for app secret creation
1212
+ secret , err := ownerClient .PostOAuth2ProviderAppSecret (ctx , apps .Default .ID )
1213
+ require .NoError (t , err )
1214
+
1215
+ cfg := & oauth2.Config {
1216
+ ClientID : apps .Default .ID .String (),
1217
+ ClientSecret : secret .ClientSecretFull ,
1218
+ Endpoint : oauth2.Endpoint {
1219
+ AuthURL : apps .Default .Endpoints .Authorization ,
1220
+ DeviceAuthURL : apps .Default .Endpoints .DeviceAuth ,
1221
+ TokenURL : apps .Default .Endpoints .Token ,
1222
+ AuthStyle : oauth2 .AuthStyleInParams ,
1223
+ },
1224
+ RedirectURL : apps .Default .CallbackURL ,
1225
+ Scopes : []string {},
1226
+ }
1227
+
1228
+ // Authorization and token exchange
1229
+ code , err := authorizationFlow (ctx , ownerClient , cfg )
1230
+ require .NoError (t , err )
1231
+ tok , err := cfg .Exchange (ctx , code )
1232
+ require .NoError (t , err )
1233
+ require .NotEmpty (t , tok .AccessToken )
1234
+ require .NotEmpty (t , tok .RefreshToken )
1235
+
1236
+ // Parse refresh token prefix (coder_<prefix>_<secret>)
1237
+ parts := strings .Split (tok .RefreshToken , "_" )
1238
+ require .Len (t , parts , 3 )
1239
+ prefix := parts [1 ]
1240
+
1241
+ // Look up refresh token row and associated API key
1242
+ dbToken , err := db .GetOAuth2ProviderAppTokenByPrefix (dbauthz .AsSystemRestricted (ctx ), []byte (prefix ))
1243
+ require .NoError (t , err )
1244
+ apiKey , err := db .GetAPIKeyByID (dbauthz .AsSystemRestricted (ctx ), dbToken .APIKeyID )
1245
+ require .NoError (t , err )
1246
+
1247
+ // Assert refresh token expiry is strictly after access token expiry
1248
+ require .Truef (t , dbToken .ExpiresAt .After (apiKey .ExpiresAt ),
1249
+ "expected refresh expiry %s to be after access expiry %s" ,
1250
+ dbToken .ExpiresAt , apiKey .ExpiresAt ,
1251
+ )
1252
+ }
1253
+
1187
1254
// customTokenExchange performs a custom OAuth2 token exchange with support for resource parameter
1188
1255
// This is needed because golang.org/x/oauth2 doesn't support custom parameters in token requests
1189
1256
func customTokenExchange (ctx context.Context , baseURL , clientID , clientSecret , code , redirectURI , resource string ) (* oauth2.Token , error ) {
0 commit comments