@@ -44,6 +44,32 @@ vi.mock('@/lib/auth/connectors/managed-oauth', () => ({
4444 } ,
4545 }
4646 }
47+ if ( providerId === 'monday' ) {
48+ return {
49+ providerId,
50+ clientId : 'monday-client-1' ,
51+ clientSecret : 'monday-secret-1' ,
52+ authorizationUrl : 'https://auth.monday.com/oauth2/authorize' ,
53+ tokenUrl : 'https://auth.monday.com/oauth_ms/oauth/token' ,
54+ redirectURI : 'https://sim.example.com/api/auth/oauth2/callback/monday' ,
55+ scopes : [ 'boards:read' , 'me:read' ] ,
56+ responseType : 'code' ,
57+ authentication : 'post' ,
58+ getToken : mockGetToken ,
59+ managedOAuth : {
60+ additionalScopes : [ ] ,
61+ requiresRefreshToken : true ,
62+ pkce : true ,
63+ nonceVerification : 'state_only' ,
64+ includeLoginHint : false ,
65+ getAuthorizationAppId : ( clientId : string ) => `monday:${ clientId } ` ,
66+ verifyIdentity : mockVerifyIdentity ,
67+ hasRequiredScopes : ( granted : string [ ] , required : string [ ] ) =>
68+ required . every ( ( scope ) => granted . includes ( scope ) ) ,
69+ isTerminalRefreshError : ( errorCode : string | undefined ) => errorCode === 'invalid_grant' ,
70+ } ,
71+ }
72+ }
4773 if ( providerId === 'jira' ) {
4874 return {
4975 providerId,
@@ -82,6 +108,7 @@ import { createStandardOAuthCredentialGroupProviderAdapter } from '@/lib/credent
82108
83109const adapter = createStandardOAuthCredentialGroupProviderAdapter ( 'google-calendar' )
84110const jiraAdapter = createStandardOAuthCredentialGroupProviderAdapter ( 'jira' )
111+ const mondayAdapter = createStandardOAuthCredentialGroupProviderAdapter ( 'monday' )
85112
86113function buildContext ( ) : CredentialGroupOAuthContext {
87114 return {
@@ -210,6 +237,82 @@ describe('standard OAuth Credential Group provider', () => {
210237 } )
211238 } )
212239
240+ it ( 'uses PKCE and persists expiring rotating credentials for managed Monday OAuth' , async ( ) => {
241+ const requiredScopes = [ 'boards:read' , 'me:read' ]
242+ const context : CredentialGroupOAuthContext = {
243+ ...buildContext ( ) ,
244+ option : {
245+ ...buildContext ( ) . option ,
246+ provider : 'monday' ,
247+ label : 'Monday.com' ,
248+ authorizationAppId : 'monday:monday-client-1' ,
249+ requiredScopes,
250+ } ,
251+ }
252+ const policy = await mondayAdapter . getPolicy ( context . option , {
253+ workspaceId : context . workspaceId ,
254+ credentialGroupId : context . credentialGroupId ,
255+ } )
256+ const prepared = await mondayAdapter . prepareAuthorization ( context , policy )
257+ const authorizationUrl = new URL (
258+ await prepared . buildAuthorizationUrl ( { state : 'monday-state-1' , nonce : 'nonce-ignored' } )
259+ )
260+
261+ expect ( mondayAdapter . requiresRefreshToken ) . toBe ( true )
262+ expect ( prepared . codeVerifier ) . toHaveLength ( 86 )
263+ expect ( authorizationUrl . searchParams . get ( 'code_challenge_method' ) ) . toBe ( 'S256' )
264+ expect ( authorizationUrl . searchParams . get ( 'code_challenge' ) ) . toBeTruthy ( )
265+
266+ const accessTokenExpiresAt = new Date ( '2026-08-14T01:00:00Z' )
267+ mockGetToken . mockResolvedValueOnce ( {
268+ tokenType : 'Bearer' ,
269+ accessToken : 'monday-access-1' ,
270+ refreshToken : 'monday-refresh-1' ,
271+ accessTokenExpiresAt,
272+ scopes : requiredScopes ,
273+ } )
274+ mockVerifyIdentity . mockResolvedValueOnce ( {
275+ providerSubjectId : 'monday-user-1' ,
276+ providerTenantId : null ,
277+ email : 'person@example.com' ,
278+ emailVerified : true ,
279+ grantedScopes : requiredScopes ,
280+ } )
281+
282+ const grant = await mondayAdapter . exchangeAndVerify ( {
283+ context,
284+ attempt : {
285+ state : 'monday-state-1' ,
286+ provider : 'monday' ,
287+ nonceHash : 'unused-for-state-bound-provider' ,
288+ enrollmentId : context . enrollmentId ,
289+ credentialGroupId : context . credentialGroupId ,
290+ optionId : context . option . id ,
291+ authorizationAppId : policy . authorizationAppId ,
292+ scopeVersion : policy . scopeVersion ,
293+ requiredScopes,
294+ redirectUri : prepared . redirectUri ,
295+ codeVerifier : prepared . codeVerifier ,
296+ invitationToken : 'invitation-1' ,
297+ createdAt : Date . now ( ) ,
298+ } ,
299+ code : 'monday-code-1' ,
300+ policy,
301+ } )
302+
303+ expect ( mockGetToken ) . toHaveBeenLastCalledWith ( {
304+ code : 'monday-code-1' ,
305+ redirectURI : 'https://sim.example.com/api/auth/oauth2/callback/monday' ,
306+ codeVerifier : prepared . codeVerifier ,
307+ } )
308+ expect ( grant ) . toMatchObject ( {
309+ accessToken : 'monday-access-1' ,
310+ refreshToken : 'monday-refresh-1' ,
311+ accessTokenExpiresAt,
312+ grantedScopes : requiredScopes ,
313+ } )
314+ } )
315+
213316 it ( 'rejects a different invited email' , async ( ) => {
214317 mockVerifyIdentity . mockResolvedValueOnce ( {
215318 providerSubjectId : 'google-sub-2' ,
0 commit comments