Merge pull request #15841 from headphonejames/redirect-oauth-flow
Oauth2 redirect flow for authentication (instead of using popup)
This commit is contained in:
@@ -83,56 +83,10 @@ describe('OAuthAdapter', () => {
|
||||
callbackUrl: 'http://domain.org/auth/test-provider/handler/frame',
|
||||
};
|
||||
|
||||
it('sets the correct headers in start', async () => {
|
||||
const oauthProvider = new OAuthAdapter(
|
||||
providerInstance,
|
||||
oAuthProviderOptions,
|
||||
);
|
||||
const mockRequest = {
|
||||
query: {
|
||||
scope: 'user',
|
||||
env: 'development',
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
const defaultState = { nonce: 'nonce', env: 'development' };
|
||||
|
||||
const mockResponse = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
statusCode: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
await oauthProvider.start(mockRequest, mockResponse);
|
||||
// nonce cookie checks
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
`${oAuthProviderOptions.providerId}-nonce`,
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
path: '/auth/test-provider/handler',
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
domain: 'domain.org',
|
||||
sameSite: 'lax',
|
||||
secure: false,
|
||||
}),
|
||||
);
|
||||
// redirect checks
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
|
||||
expect(mockResponse.statusCode).toEqual(301);
|
||||
expect(mockResponse.end).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sets the refresh cookie if refresh is enabled', async () => {
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
isOriginAllowed: () => false,
|
||||
});
|
||||
|
||||
const state = { nonce: 'nonce', env: 'development' };
|
||||
const mockRequest = {
|
||||
const createEncodedQueryMockRequest = (state: any) => {
|
||||
return {
|
||||
cookies: {
|
||||
'test-provider-nonce': 'nonce',
|
||||
},
|
||||
@@ -140,12 +94,69 @@ describe('OAuthAdapter', () => {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
const mockResponse = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
statusCode: jest.fn().mockReturnThis(),
|
||||
redirect: jest.fn().mockReturnThis(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
const mockStartRequest = {
|
||||
query: {
|
||||
scope: 'user',
|
||||
env: 'development',
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
|
||||
const expectedStartAuthCookieData = {
|
||||
httpOnly: true,
|
||||
path: '/auth/test-provider/handler',
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
domain: 'domain.org',
|
||||
sameSite: 'lax',
|
||||
secure: false,
|
||||
};
|
||||
|
||||
it('sets the correct headers in start', async () => {
|
||||
const oauthProvider = new OAuthAdapter(
|
||||
providerInstance,
|
||||
oAuthProviderOptions,
|
||||
);
|
||||
|
||||
await oauthProvider.start(mockStartRequest, mockResponse);
|
||||
// nonce cookie checks
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
`${oAuthProviderOptions.providerId}-nonce`,
|
||||
expect.any(String),
|
||||
expect.objectContaining(expectedStartAuthCookieData),
|
||||
);
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
|
||||
expect(mockResponse.statusCode).toEqual(301);
|
||||
expect(mockResponse.end).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const refreshCookieData = {
|
||||
...expectedStartAuthCookieData,
|
||||
path: '/auth/test-provider',
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
};
|
||||
|
||||
it('sets the refresh cookie if refresh is enabled', async () => {
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
isOriginAllowed: () => false,
|
||||
});
|
||||
|
||||
const mockRequest = createEncodedQueryMockRequest(defaultState);
|
||||
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockCookieConfigurer).toHaveBeenCalledTimes(1);
|
||||
@@ -153,15 +164,26 @@ describe('OAuthAdapter', () => {
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
expect.stringContaining('test-provider-refresh-token'),
|
||||
expect.stringContaining('token'),
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
path: '/auth/test-provider',
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
domain: 'domain.org',
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
}),
|
||||
expect.objectContaining(refreshCookieData),
|
||||
);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the refresh cookie if refresh is enabled with redirect', async () => {
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
isOriginAllowed: () => false,
|
||||
});
|
||||
|
||||
const state = {
|
||||
...defaultState,
|
||||
redirectUrl: 'http://localhost:3000',
|
||||
flow: 'redirect',
|
||||
};
|
||||
const mockRequest = createEncodedQueryMockRequest(state);
|
||||
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('persists scope through cookie if enabled', async () => {
|
||||
@@ -179,32 +201,15 @@ describe('OAuthAdapter', () => {
|
||||
});
|
||||
|
||||
// First we test the /start request, making sure state is set
|
||||
const mockStartReq = {
|
||||
query: {
|
||||
scope: 'user',
|
||||
env: 'development',
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
const mockStartRes = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
statusCode: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
await oauthProvider.start(mockStartReq, mockStartRes);
|
||||
await oauthProvider.start(mockStartRequest, mockResponse);
|
||||
|
||||
expect(handlers.start).toHaveBeenCalledTimes(1);
|
||||
expect(handlers.start).toHaveBeenCalledWith({
|
||||
query: {
|
||||
scope: 'user',
|
||||
env: 'development',
|
||||
},
|
||||
...mockStartRequest,
|
||||
scope: 'user',
|
||||
state: {
|
||||
nonce: expect.any(String),
|
||||
env: 'development',
|
||||
origin: undefined,
|
||||
scope: 'user',
|
||||
},
|
||||
});
|
||||
@@ -223,6 +228,7 @@ describe('OAuthAdapter', () => {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
redirect: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
await oauthProvider.frameHandler(mockHandleReq, mockHandleRes);
|
||||
@@ -230,17 +236,11 @@ describe('OAuthAdapter', () => {
|
||||
expect(mockHandleRes.cookie).toHaveBeenCalledWith(
|
||||
'test-provider-granted-scope',
|
||||
'user',
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
path: '/auth/test-provider',
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
domain: 'domain.org',
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
}),
|
||||
expect.objectContaining(refreshCookieData),
|
||||
);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
|
||||
// Them make sure scopes are forwarded correctly during refresh
|
||||
// Then make sure scopes are forwarded correctly during refresh
|
||||
const mockRefreshReq = {
|
||||
query: { scope: 'ignore-me' },
|
||||
cookies: {
|
||||
@@ -252,6 +252,7 @@ describe('OAuthAdapter', () => {
|
||||
const mockRefreshRes = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
redirect: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
await oauthProvider.refresh(mockRefreshReq, mockRefreshRes);
|
||||
expect(handlers.refresh).toHaveBeenCalledTimes(1);
|
||||
@@ -261,8 +262,18 @@ describe('OAuthAdapter', () => {
|
||||
refreshToken: 'refresh-token',
|
||||
}),
|
||||
);
|
||||
expect(mockRefreshRes.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const mockRequestWithHeader = {
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: {
|
||||
'test-provider-refresh-token': 'token',
|
||||
},
|
||||
query: {},
|
||||
get: jest.fn(),
|
||||
} as unknown as express.Request;
|
||||
|
||||
it('removes refresh cookie and calls logout handler when logging out', async () => {
|
||||
const logoutSpy = jest.spyOn(providerInstance, 'logout');
|
||||
const oauthProvider = new OAuthAdapter(providerInstance, {
|
||||
@@ -270,22 +281,8 @@ describe('OAuthAdapter', () => {
|
||||
isOriginAllowed: () => false,
|
||||
});
|
||||
|
||||
const mockRequest = {
|
||||
cookies: {
|
||||
'test-provider-refresh-token': 'token',
|
||||
},
|
||||
header: () => 'XMLHttpRequest',
|
||||
get: jest.fn(),
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
await oauthProvider.logout(mockRequest, mockResponse);
|
||||
expect(mockRequest.get).toHaveBeenCalledTimes(1);
|
||||
await oauthProvider.logout(mockRequestWithHeader, mockResponse);
|
||||
expect(mockRequestWithHeader.get).toHaveBeenCalledTimes(1);
|
||||
expect(logoutSpy).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
@@ -294,6 +291,7 @@ describe('OAuthAdapter', () => {
|
||||
expect.objectContaining({ path: '/auth/test-provider' }),
|
||||
);
|
||||
expect(mockResponse.end).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('gets new access-token when refreshing', async () => {
|
||||
@@ -302,20 +300,7 @@ describe('OAuthAdapter', () => {
|
||||
isOriginAllowed: () => false,
|
||||
});
|
||||
|
||||
const mockRequest = {
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: {
|
||||
'test-provider-refresh-token': 'token',
|
||||
},
|
||||
query: {},
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
json: jest.fn().mockReturnThis(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
await oauthProvider.refresh(mockRequest, mockResponse);
|
||||
await oauthProvider.refresh(mockRequestWithHeader, mockResponse);
|
||||
expect(mockResponse.json).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
...mockResponseData,
|
||||
@@ -328,6 +313,7 @@ describe('OAuthAdapter', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets new access-token when old cookie exists', async () => {
|
||||
@@ -337,20 +323,12 @@ describe('OAuthAdapter', () => {
|
||||
});
|
||||
|
||||
const mockRequest = {
|
||||
header: () => 'XMLHttpRequest',
|
||||
...mockRequestWithHeader,
|
||||
cookies: {
|
||||
'test-provider-refresh-token': 'old-token',
|
||||
},
|
||||
query: {},
|
||||
get: jest.fn(),
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
json: jest.fn().mockReturnThis(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
await oauthProvider.refresh(mockRequest, mockResponse);
|
||||
expect(mockRequest.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockCookieConfigurer).toHaveBeenCalledTimes(1);
|
||||
@@ -358,15 +336,9 @@ describe('OAuthAdapter', () => {
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
'test-provider-refresh-token',
|
||||
'token',
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
path: '/auth/test-provider',
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
domain: 'domain.org',
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
}),
|
||||
expect.objectContaining(refreshCookieData),
|
||||
);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the correct nonce cookie configuration', async () => {
|
||||
@@ -380,162 +352,89 @@ describe('OAuthAdapter', () => {
|
||||
...oAuthProviderOptions,
|
||||
});
|
||||
|
||||
const mockRequest = {
|
||||
query: {
|
||||
scope: 'user',
|
||||
env: 'development',
|
||||
origin: 'http://domain.org',
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
statusCode: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
await oauthProvider.start(mockRequest, mockResponse);
|
||||
await oauthProvider.start(mockStartRequest, mockResponse);
|
||||
expect(mockCookieConfigurer).not.toHaveBeenCalled();
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
`${oAuthProviderOptions.providerId}-nonce`,
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
domain: 'domain.org',
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
path: '/auth/test-provider/handler',
|
||||
secure: false,
|
||||
sameSite: 'lax',
|
||||
}),
|
||||
expect.objectContaining(expectedStartAuthCookieData),
|
||||
);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the correct nonce cookie configuration using origin from request', async () => {
|
||||
const config = {
|
||||
baseUrl: 'http://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => false,
|
||||
};
|
||||
const config = {
|
||||
baseUrl: 'http://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => false,
|
||||
};
|
||||
|
||||
const mockStartRequestWithOrigin = {
|
||||
query: {
|
||||
scope: 'user',
|
||||
env: 'development',
|
||||
origin: 'http://other.domain',
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
|
||||
it('sets the correct nonce cookie configuration using origin from request', async () => {
|
||||
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
|
||||
});
|
||||
|
||||
const mockRequest = {
|
||||
query: {
|
||||
scope: 'user',
|
||||
env: 'development',
|
||||
origin: 'http://other.domain',
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
statusCode: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
await oauthProvider.start(mockRequest, mockResponse);
|
||||
await oauthProvider.start(mockStartRequestWithOrigin, mockResponse);
|
||||
expect(mockCookieConfigurer).not.toHaveBeenCalled();
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
`${oAuthProviderOptions.providerId}-nonce`,
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
domain: 'domain.org',
|
||||
maxAge: TEN_MINUTES_MS,
|
||||
path: '/auth/test-provider/handler',
|
||||
...expectedStartAuthCookieData,
|
||||
secure: true,
|
||||
sameSite: 'none',
|
||||
}),
|
||||
);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the correct cookie configuration using an secure callbackUrl', async () => {
|
||||
const config = {
|
||||
baseUrl: 'https://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => false,
|
||||
};
|
||||
const secureCookieData = {
|
||||
...refreshCookieData,
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
};
|
||||
|
||||
it('sets the correct cookie configuration using an secure callbackUrl', async () => {
|
||||
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
|
||||
});
|
||||
|
||||
const state = {
|
||||
nonce: 'nonce',
|
||||
env: 'development',
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
cookies: {
|
||||
'test-provider-nonce': 'nonce',
|
||||
},
|
||||
query: {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
const mockRequest = createEncodedQueryMockRequest(defaultState);
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockCookieConfigurer).not.toHaveBeenCalled();
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
expect.stringContaining('test-provider-refresh-token'),
|
||||
expect.stringContaining('token'),
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
domain: 'domain.org',
|
||||
path: '/auth/test-provider',
|
||||
secure: true,
|
||||
sameSite: 'lax',
|
||||
}),
|
||||
expect.objectContaining(secureCookieData),
|
||||
);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the correct cookie configuration when on different domains and secure', async () => {
|
||||
const config = {
|
||||
baseUrl: 'https://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => false,
|
||||
};
|
||||
const secureSameSiteNoneCookieData = {
|
||||
...secureCookieData,
|
||||
sameSite: 'none',
|
||||
};
|
||||
|
||||
it('sets the correct cookie configuration when on different domains and secure', async () => {
|
||||
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
callbackUrl: 'https://authdomain.org/auth/test-provider/handler/frame',
|
||||
});
|
||||
|
||||
const state = {
|
||||
nonce: 'nonce',
|
||||
env: 'development',
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
cookies: {
|
||||
'test-provider-nonce': 'nonce',
|
||||
},
|
||||
query: {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
const mockRequest = createEncodedQueryMockRequest(defaultState);
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockCookieConfigurer).not.toHaveBeenCalled();
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
@@ -543,106 +442,108 @@ describe('OAuthAdapter', () => {
|
||||
expect.stringContaining('test-provider-refresh-token'),
|
||||
expect.stringContaining('token'),
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
...secureSameSiteNoneCookieData,
|
||||
domain: 'authdomain.org',
|
||||
path: '/auth/test-provider',
|
||||
secure: true,
|
||||
sameSite: 'none',
|
||||
}),
|
||||
);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const configOriginAllowed = {
|
||||
...config,
|
||||
isOriginAllowed: () => true,
|
||||
};
|
||||
|
||||
it('sets the correct cookie configuration using origin from state', async () => {
|
||||
const config = {
|
||||
baseUrl: 'https://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => true,
|
||||
};
|
||||
const oauthProvider = OAuthAdapter.fromConfig(
|
||||
configOriginAllowed,
|
||||
providerInstance,
|
||||
{
|
||||
...oAuthProviderOptions,
|
||||
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
|
||||
},
|
||||
);
|
||||
|
||||
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
|
||||
});
|
||||
|
||||
const state = {
|
||||
nonce: 'nonce',
|
||||
env: 'development',
|
||||
const mockRequest = createEncodedQueryMockRequest({
|
||||
...defaultState,
|
||||
origin: 'http://other.domain',
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
cookies: {
|
||||
'test-provider-nonce': 'nonce',
|
||||
},
|
||||
query: {
|
||||
state: encodeState(state),
|
||||
},
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
end: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
});
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockCookieConfigurer).not.toHaveBeenCalled();
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
expect.stringContaining('test-provider-refresh-token'),
|
||||
expect.stringContaining('token'),
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
domain: 'domain.org',
|
||||
path: '/auth/test-provider',
|
||||
secure: true,
|
||||
sameSite: 'none',
|
||||
}),
|
||||
expect.objectContaining(secureSameSiteNoneCookieData),
|
||||
);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets the correct cookie configuration using origin from header', async () => {
|
||||
const config = {
|
||||
baseUrl: 'https://domain.org/auth',
|
||||
appUrl: 'http://domain.org',
|
||||
isOriginAllowed: () => false,
|
||||
};
|
||||
const mockRequestWithGetMockReturn = {
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: {
|
||||
'test-provider-refresh-token': 'old-token',
|
||||
},
|
||||
query: {},
|
||||
get: jest.fn().mockReturnValue('http://other.domain'),
|
||||
} as unknown as express.Request;
|
||||
|
||||
it('sets the correct cookie configuration using origin from header', async () => {
|
||||
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
|
||||
...oAuthProviderOptions,
|
||||
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
|
||||
});
|
||||
|
||||
const mockRequest = {
|
||||
header: () => 'XMLHttpRequest',
|
||||
cookies: {
|
||||
'test-provider-refresh-token': 'old-token',
|
||||
},
|
||||
query: {},
|
||||
get: jest.fn().mockReturnValue('http://other.domain'),
|
||||
} as unknown as express.Request;
|
||||
|
||||
const mockResponse = {
|
||||
json: jest.fn().mockReturnThis(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
cookie: jest.fn().mockReturnThis(),
|
||||
} as unknown as express.Response;
|
||||
|
||||
await oauthProvider.refresh(mockRequest, mockResponse);
|
||||
expect(mockRequest.get).toHaveBeenCalledTimes(1);
|
||||
await oauthProvider.refresh(mockRequestWithGetMockReturn, mockResponse);
|
||||
expect(mockRequestWithGetMockReturn.get).toHaveBeenCalledTimes(1);
|
||||
expect(mockCookieConfigurer).not.toHaveBeenCalled();
|
||||
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.cookie).toHaveBeenCalledWith(
|
||||
'test-provider-refresh-token',
|
||||
'token',
|
||||
expect.objectContaining({
|
||||
httpOnly: true,
|
||||
path: '/auth/test-provider',
|
||||
maxAge: THOUSAND_DAYS_MS,
|
||||
domain: 'domain.org',
|
||||
secure: true,
|
||||
sameSite: 'none',
|
||||
}),
|
||||
expect.objectContaining(secureSameSiteNoneCookieData),
|
||||
);
|
||||
expect(mockResponse.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('executed a response redirect when flow query string is set to "redirect"', async () => {
|
||||
const handlers = {
|
||||
start: jest.fn(async (_req: { state: OAuthState }) => ({
|
||||
url: '/url',
|
||||
status: 301,
|
||||
})),
|
||||
handler: jest.fn(async () => ({ response: mockResponseData })),
|
||||
refresh: jest.fn(async () => ({ response: mockResponseData })),
|
||||
};
|
||||
const configWithNoPopupEnabled = {
|
||||
...configOriginAllowed,
|
||||
};
|
||||
const oauthProvider = OAuthAdapter.fromConfig(
|
||||
configWithNoPopupEnabled,
|
||||
handlers,
|
||||
{
|
||||
...oAuthProviderOptions,
|
||||
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
|
||||
},
|
||||
);
|
||||
|
||||
const state = {
|
||||
...defaultState,
|
||||
origin: 'http://other.domain',
|
||||
redirectUrl: 'http://domain.org',
|
||||
flow: 'redirect',
|
||||
};
|
||||
|
||||
const mockRequest = {
|
||||
...createEncodedQueryMockRequest(state),
|
||||
get: jest.fn().mockReturnValue('http://other.domain'),
|
||||
} as unknown as express.Request;
|
||||
|
||||
await oauthProvider.frameHandler(mockRequest, mockResponse);
|
||||
expect(mockRequest.get).not.toHaveBeenCalled();
|
||||
expect(mockCookieConfigurer).not.toHaveBeenCalled();
|
||||
expect(mockResponse.cookie).not.toHaveBeenCalled();
|
||||
expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
|
||||
expect(mockResponse.redirect).toHaveBeenCalledWith('http://domain.org');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,7 +33,11 @@ import {
|
||||
NotAllowedError,
|
||||
} from '@backstage/errors';
|
||||
import { defaultCookieConfigurer, readState, verifyNonce } from './helpers';
|
||||
import { postMessageResponse, ensuresXRequestedWith } from '../flow';
|
||||
import {
|
||||
postMessageResponse,
|
||||
ensuresXRequestedWith,
|
||||
WebMessageResponse,
|
||||
} from '../flow';
|
||||
import {
|
||||
OAuthHandlers,
|
||||
OAuthStartRequest,
|
||||
@@ -98,6 +102,8 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
const scope = req.query.scope?.toString() ?? '';
|
||||
const env = req.query.env?.toString();
|
||||
const origin = req.query.origin?.toString();
|
||||
const redirectUrl = req.query.redirectUrl?.toString();
|
||||
const flow = req.query.flow?.toString();
|
||||
|
||||
if (!env) {
|
||||
throw new InputError('No env provided in request query parameters');
|
||||
@@ -109,7 +115,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
// set a nonce cookie before redirecting to oauth provider
|
||||
this.setNonceCookie(res, nonce, cookieConfig);
|
||||
|
||||
const state: OAuthState = { nonce, env, origin };
|
||||
const state: OAuthState = { nonce, env, origin, redirectUrl, flow };
|
||||
|
||||
// If scopes are persisted then we pass them through the state so that we
|
||||
// can set the cookie on successful auth
|
||||
@@ -169,11 +175,21 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
|
||||
|
||||
const identity = await this.populateIdentity(response.backstageIdentity);
|
||||
|
||||
// post message back to popup if successful
|
||||
return postMessageResponse(res, appOrigin, {
|
||||
const responseObj: WebMessageResponse = {
|
||||
type: 'authorization_response',
|
||||
response: { ...response, backstageIdentity: identity },
|
||||
});
|
||||
};
|
||||
|
||||
if (state.flow === 'redirect') {
|
||||
if (!state.redirectUrl) {
|
||||
throw new InputError(
|
||||
'No redirectUrl provided in request query parameters',
|
||||
);
|
||||
}
|
||||
res.redirect(state.redirectUrl);
|
||||
}
|
||||
// post message back to popup if successful
|
||||
return postMessageResponse(res, appOrigin, responseObj);
|
||||
} catch (error) {
|
||||
const { name, message } = isError(error)
|
||||
? error
|
||||
|
||||
@@ -90,6 +90,8 @@ export type OAuthState = {
|
||||
env: string;
|
||||
origin?: string;
|
||||
scope?: string;
|
||||
redirectUrl?: string;
|
||||
flow?: string;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
|
||||
Reference in New Issue
Block a user