@@ -576,7 +576,15 @@ async function authorizeThroughProfile(
576576 account : TEST_LOGIN_ACCOUNT ,
577577 password : TEST_LOGIN_PASSWORD ,
578578 } ) ;
579- assert . equal ( login . status , 302 ) ;
579+ assert . ok ( login . status === 302 || login . status === 303 ) ;
580+ if ( ! scope . split ( / \s + / ) . includes ( "email" ) ) {
581+ return {
582+ response : login ,
583+ codeVerifier : verifier ,
584+ profileLocation : undefined ,
585+ interactionUid : extractInteractionUid ( interactionLocation ) ,
586+ } ;
587+ }
580588 assert . match (
581589 login . headers [ "location" ] as string ,
582590 / \/ i n t e r a c t i o n \/ .+ \/ p r o f i l e / ,
@@ -696,12 +704,13 @@ async function openLoginInteraction(
696704 agent : any ,
697705 state = "login-state-1" ,
698706 headers ?: Record < string , string > ,
707+ scope = "openid profile email" ,
699708) {
700709 const authorize = await withHeaders ( agent . get ( "/auth" ) , headers ) . query ( {
701710 client_id : "demo-site" ,
702711 redirect_uri : TEST_REDIRECT_URI ,
703712 response_type : "code" ,
704- scope : "openid profile" ,
713+ scope,
705714 prompt : "consent" ,
706715 state,
707716 nonce : "nonce-login-1" ,
@@ -1157,7 +1166,7 @@ test("seeded demo client is confidential web client", async () => {
11571166} ) ;
11581167
11591168test ( "public client without explicit refresh confirmation does not receive refresh token" , async ( ) => {
1160- const { app, state, emailSender } = await createTestApp ( ) ;
1169+ const { app, state } = await createTestApp ( ) ;
11611170 await upsertPublicNoneClient ( state , "public-unconfirmed" , {
11621171 allowRefreshTokenForPublicClient : false ,
11631172 } ) ;
@@ -1187,31 +1196,9 @@ test("public client without explicit refresh confirmation does not receive refre
11871196 account : TEST_LOGIN_ACCOUNT ,
11881197 password : TEST_LOGIN_PASSWORD ,
11891198 } ) ;
1190- assert . equal ( login . status , 302 ) ;
1191- const profileLocation = login . headers [ "location" ] as string ;
1192- const profilePage = await agent . get ( profileLocation ) ;
1193- const sendCode = await agent
1194- . post ( profileLocation )
1195- . type ( "form" )
1196- . send ( {
1197- csrf : extractCsrf ( profilePage . text ) ,
1198- action : "send_code" ,
1199- email : "demo@example.com" ,
1200- } ) ;
1201- const sentCode = emailSender . latestCode (
1202- extractInteractionUid ( interactionLocation ) ,
1203- "demo@example.com" ,
1204- ) ;
1205- assert . equal ( typeof sentCode , "string" ) ;
1206- const profile = await agent
1207- . post ( profileLocation )
1208- . type ( "form" )
1209- . send ( {
1210- csrf : extractCsrf ( sendCode . text ) ,
1211- action : "verify_code" ,
1212- code : sentCode ,
1213- } ) ;
1214- const consentPageHtml = await followToConsentPage ( agent , profile ) ;
1199+ assert . ok ( login . status === 302 || login . status === 303 ) ;
1200+ assert . doesNotMatch ( login . headers [ "location" ] as string , / \/ p r o f i l e / ) ;
1201+ const consentPageHtml = await followToConsentPage ( agent , login ) ;
12151202 const consent = await agent
12161203 . post ( normalizeActionPath ( extractConsentAction ( consentPageHtml ) ) )
12171204 . type ( "form" )
@@ -2004,6 +1991,42 @@ test("profile routes reject requests without the interaction session cookie", as
20041991 await state . store . close ( ) ;
20051992} ) ;
20061993
1994+ test ( "login without email scope skips profile completion" , async ( ) => {
1995+ const { app, state, emailSender } = await createTestApp ( ) ;
1996+ const agent = request . agent ( app ) ;
1997+ const { interactionLocation, loginPage } = await openLoginInteraction (
1998+ agent ,
1999+ "login-without-email-scope" ,
2000+ undefined ,
2001+ "openid profile" ,
2002+ ) ;
2003+ const login = await agent
2004+ . post ( `${ interactionLocation } /login` )
2005+ . type ( "form" )
2006+ . send ( {
2007+ csrf : extractCsrf ( loginPage . text ) ,
2008+ account : TEST_LOGIN_ACCOUNT ,
2009+ password : TEST_LOGIN_PASSWORD ,
2010+ } ) ;
2011+
2012+ assert . ok ( login . status === 302 || login . status === 303 ) ;
2013+ assert . doesNotMatch ( login . headers [ "location" ] as string , / \/ p r o f i l e / ) ;
2014+ const callback = await followToRedirectUriOrigin (
2015+ agent ,
2016+ login ,
2017+ TEST_REDIRECT_URI ,
2018+ ) ;
2019+ const callbackUrl = new URL ( callback ) ;
2020+ assert . equal (
2021+ callbackUrl . searchParams . get ( "state" ) ,
2022+ "login-without-email-scope" ,
2023+ ) ;
2024+ assert . equal ( typeof callbackUrl . searchParams . get ( "code" ) , "string" ) ;
2025+ assert . equal ( emailSender . sentVerifications . length , 0 ) ;
2026+
2027+ await state . store . close ( ) ;
2028+ } ) ;
2029+
20072030test ( "interactive login treats upstream outages as retryable 503 without consuming the failure budget" , async ( ) => {
20082031 const { app, state } = await createTestApp ( {
20092032 // Isolate the failure-bucket behavior from the separate attempt limiter.
0 commit comments