diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index ce4b52ef4f..ad1b6a316f 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -35,7 +35,7 @@ import { NotAllowedError, } from '@backstage/errors'; import { TokenIssuer } from '../../identity/types'; -import { readState, verifyNonce } from './helpers'; +import { getCookieConfig, readState, verifyNonce } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; import { OAuthHandlers, @@ -58,25 +58,32 @@ export type Options = { appOrigin: string; tokenIssuer: TokenIssuer; isOriginAllowed: (origin: string) => boolean; + callbackUrl?: string; }; - export class OAuthAdapter implements AuthProviderRouteHandlers { static fromConfig( config: AuthProviderConfig, handlers: OAuthHandlers, options: Pick< Options, - 'providerId' | 'persistScopes' | 'disableRefresh' | 'tokenIssuer' + | 'providerId' + | 'persistScopes' + | 'disableRefresh' + | 'tokenIssuer' + | 'callbackUrl' >, ): OAuthAdapter { const { origin: appOrigin } = new URL(config.appUrl); - const secure = config.baseUrl.startsWith('https://'); - const url = new URL(config.baseUrl); - const cookiePath = `${url.pathname}/${options.providerId}`; + const authUrl = new URL(options.callbackUrl ?? config.baseUrl); + const { cookieDomain, cookiePath, secure } = getCookieConfig( + authUrl, + options.providerId, + ); + return new OAuthAdapter(handlers, { ...options, appOrigin, - cookieDomain: url.hostname, + cookieDomain, cookiePath, secure, isOriginAllowed: config.isOriginAllowed, @@ -263,7 +270,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { secure: this.options.secure, sameSite: 'lax', domain: this.options.cookieDomain, - path: `${this.options.cookiePath}/handler`, + path: this.options.cookiePath, httpOnly: true, }); }; @@ -274,7 +281,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { secure: this.options.secure, sameSite: 'lax', domain: this.options.cookieDomain, - path: `${this.options.cookiePath}/handler`, + path: this.options.cookiePath, httpOnly: true, }); }; diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 2bf8fa2520..635c1eb39b 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -57,3 +57,27 @@ export const verifyNonce = (req: express.Request, providerId: string) => { throw new Error('Invalid nonce'); } }; + +export const getCookieConfig = (authUrl: URL, providerId: string) => { + const { hostname: cookieDomain, pathname, protocol } = authUrl; + const secure = protocol === 'https:'; + + // If the provider supports callbackUrls, the pathname will + // contain the complete path to the frame handler. + // The regex captures the leading path including the provider. + // + // Example match: /api/auth/provider/handler/frame + // Example result: /api/auth/provider + const matcher = pathname.match( + new RegExp(`(?^\/.*?\/${providerId})\/handler\/frame$`), + )?.groups; + + // If there's no match we can manually construct the path + const cookiePath = matcher?.path ?? `${pathname}/${providerId}`; + + return { + cookieDomain, + cookiePath, + secure, + }; +}; diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 220ec4fcce..a1d2ebb062 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -313,6 +313,7 @@ export const createGithubProvider = ( persistScopes: true, providerId, tokenIssuer, + callbackUrl, }); }); };