Skip to content

Commit 2beb71a

Browse files
committed
feat(security,uiam): support both ES native and UIAM session tokens when in UIAM mode
1 parent 12e5819 commit 2beb71a

File tree

2 files changed

+282
-54
lines changed

2 files changed

+282
-54
lines changed

x-pack/platform/plugins/shared/security/server/authentication/providers/saml.test.ts

Lines changed: 250 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,14 +1742,14 @@ describe('SAMLAuthenticationProvider', () => {
17421742
});
17431743

17441744
mockOptions.client.asInternalUser.transport.request.mockResolvedValue({
1745-
access_token: 'some-token',
1746-
refresh_token: 'some-refresh-token',
1745+
access_token: 'essu_dev_some-token',
1746+
refresh_token: 'essu_dev_some-refresh-token',
17471747
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
17481748
authentication: mockUser,
17491749
});
17501750
mockOptions.uiam?.getUserProfileGrant.mockReturnValue({
17511751
type: 'uiamAccessToken',
1752-
accessToken: 'some-token',
1752+
accessToken: 'essu_dev_some-token',
17531753
sharedSecret: 'some-secret',
17541754
});
17551755
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
@@ -1777,20 +1777,20 @@ describe('SAMLAuthenticationProvider', () => {
17771777
AuthenticationResult.redirectTo('/test-base-path/some-path#some-app', {
17781778
userProfileGrant: {
17791779
type: 'uiamAccessToken',
1780-
accessToken: 'some-token',
1780+
accessToken: 'essu_dev_some-token',
17811781
sharedSecret: 'some-secret',
17821782
},
17831783
state: {
1784-
accessToken: 'some-token',
1785-
refreshToken: 'some-refresh-token',
1784+
accessToken: 'essu_dev_some-token',
1785+
refreshToken: 'essu_dev_some-refresh-token',
17861786
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
17871787
},
17881788
user: mockUser,
17891789
})
17901790
);
17911791

17921792
expect(mockOptions.uiam?.getUserProfileGrant).toHaveBeenCalledTimes(1);
1793-
expect(mockOptions.uiam?.getUserProfileGrant).toHaveBeenCalledWith('some-token');
1793+
expect(mockOptions.uiam?.getUserProfileGrant).toHaveBeenCalledWith('essu_dev_some-token');
17941794
expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({
17951795
method: 'POST',
17961796
path: '/_security/saml/authenticate',
@@ -1807,8 +1807,8 @@ describe('SAMLAuthenticationProvider', () => {
18071807
it('properly constructs authentication headers when UIAM is enabled.', async () => {
18081808
const request = httpServerMock.createKibanaRequest({ headers: {} });
18091809
const state = {
1810-
accessToken: 'some-valid-token',
1811-
refreshToken: 'some-valid-refresh-token',
1810+
accessToken: 'essu_dev_some-valid-token',
1811+
refreshToken: 'essu_dev_some-valid-refresh-token',
18121812
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
18131813
};
18141814
const authorization = `Bearer ${state.accessToken}`;
@@ -1841,8 +1841,8 @@ describe('SAMLAuthenticationProvider', () => {
18411841

18421842
it('fails if token invalidation fails.', async () => {
18431843
const request = httpServerMock.createKibanaRequest();
1844-
const accessToken = 'x-saml-token';
1845-
const refreshToken = 'x-saml-refresh-token';
1844+
const accessToken = 'essu_dev_x-saml-token';
1845+
const refreshToken = 'essu_dev_x-saml-refresh-token';
18461846

18471847
const failureReason = new errors.ResponseError(
18481848
securityMock.createApiResponse({ statusCode: 500, body: {} })
@@ -1859,17 +1859,17 @@ describe('SAMLAuthenticationProvider', () => {
18591859

18601860
expect(mockOptions.uiam?.invalidateSessionTokens).toHaveBeenCalledTimes(1);
18611861
expect(mockOptions.uiam?.invalidateSessionTokens).toHaveBeenCalledWith(
1862-
'x-saml-token',
1863-
'x-saml-refresh-token'
1862+
'essu_dev_x-saml-token',
1863+
'essu_dev_x-saml-refresh-token'
18641864
);
18651865

18661866
expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled();
18671867
});
18681868

18691869
it('redirects to `loggedOut` URL.', async () => {
18701870
const request = httpServerMock.createKibanaRequest();
1871-
const accessToken = 'x-saml-token';
1872-
const refreshToken = 'x-saml-refresh-token';
1871+
const accessToken = 'essu_dev_x-saml-token';
1872+
const refreshToken = 'essu_dev_x-saml-refresh-token';
18731873

18741874
await expect(
18751875
provider.logout(request, {
@@ -1881,8 +1881,8 @@ describe('SAMLAuthenticationProvider', () => {
18811881

18821882
expect(mockOptions.uiam?.invalidateSessionTokens).toHaveBeenCalledTimes(1);
18831883
expect(mockOptions.uiam?.invalidateSessionTokens).toHaveBeenCalledWith(
1884-
'x-saml-token',
1885-
'x-saml-refresh-token'
1884+
'essu_dev_x-saml-token',
1885+
'essu_dev_x-saml-refresh-token'
18861886
);
18871887

18881888
expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled();
@@ -1893,8 +1893,8 @@ describe('SAMLAuthenticationProvider', () => {
18931893
it('succeeds if token from the state is expired, but has been successfully refreshed.', async () => {
18941894
const request = httpServerMock.createKibanaRequest();
18951895
const state = {
1896-
accessToken: 'expired-token',
1897-
refreshToken: 'valid-refresh-token',
1896+
accessToken: 'essu_dev_expired-token',
1897+
refreshToken: 'essu_dev_valid-refresh-token',
18981898
realm: 'cloud-saml-kibana',
18991899
};
19001900

@@ -1903,27 +1903,27 @@ describe('SAMLAuthenticationProvider', () => {
19031903
);
19041904

19051905
mockOptions.uiam?.refreshSessionTokens.mockResolvedValue({
1906-
accessToken: 'new-access-token',
1907-
refreshToken: 'new-refresh-token',
1906+
accessToken: 'essu_dev_new-access-token',
1907+
refreshToken: 'essu_dev_new-refresh-token',
19081908
});
19091909

19101910
mockOptions.uiam?.getUserProfileGrant.mockReturnValue({
1911-
accessToken: 'new-access-token',
1911+
accessToken: 'essu_dev_new-access-token',
19121912
sharedSecret: 'some-secret',
19131913
type: 'uiamAccessToken',
19141914
});
19151915

19161916
await expect(provider.authenticate(request, state)).resolves.toEqual(
19171917
AuthenticationResult.succeeded(mockUser, {
1918-
authHeaders: { authorization: 'Bearer new-access-token' },
1918+
authHeaders: { authorization: 'Bearer essu_dev_new-access-token' },
19191919
userProfileGrant: {
1920-
accessToken: 'new-access-token',
1920+
accessToken: 'essu_dev_new-access-token',
19211921
sharedSecret: 'some-secret',
19221922
type: 'uiamAccessToken',
19231923
},
19241924
state: {
1925-
accessToken: 'new-access-token',
1926-
refreshToken: 'new-refresh-token',
1925+
accessToken: 'essu_dev_new-access-token',
1926+
refreshToken: 'essu_dev_new-refresh-token',
19271927
realm: 'cloud-saml-kibana',
19281928
},
19291929
})
@@ -1933,13 +1933,14 @@ describe('SAMLAuthenticationProvider', () => {
19331933
expect(mockOptions.uiam?.refreshSessionTokens).toHaveBeenCalledWith(state.refreshToken);
19341934

19351935
expect(request.headers).not.toHaveProperty('authorization');
1936+
expect(request.headers).not.toHaveProperty(ES_CLIENT_AUTHENTICATION_HEADER);
19361937
});
19371938

19381939
it('fails if token from the state is expired, refresh attempt failed, and displays error from UIAM', async () => {
19391940
const request = httpServerMock.createKibanaRequest({ headers: {} });
19401941
const state = {
1941-
accessToken: 'expired-token',
1942-
refreshToken: 'invalid-refresh-token',
1942+
accessToken: 'essu_dev_expired-token',
1943+
refreshToken: 'essu_dev_invalid-refresh-token',
19431944
realm: 'cloud-saml-kibana',
19441945
};
19451946
const authorization = `Bearer ${state.accessToken}`;
@@ -1963,6 +1964,227 @@ describe('SAMLAuthenticationProvider', () => {
19631964
});
19641965

19651966
expect(request.headers).not.toHaveProperty('authorization');
1967+
expect(request.headers).not.toHaveProperty(ES_CLIENT_AUTHENTICATION_HEADER);
1968+
});
1969+
});
1970+
});
1971+
1972+
describe('UIAM mode with ES native tokens', () => {
1973+
beforeEach(() => {
1974+
mockUser = mockAuthenticatedUser({
1975+
authentication_provider: { type: 'saml', name: ELASTIC_CLOUD_SSO_REALM_NAME },
1976+
});
1977+
mockOptions = mockAuthenticationProviderOptions({
1978+
name: ELASTIC_CLOUD_SSO_REALM_NAME,
1979+
uiam: true,
1980+
});
1981+
1982+
mockOptions.client.asInternalUser.transport.request.mockResolvedValue({
1983+
access_token: 'x_essu_dev_some-token',
1984+
refresh_token: 'x_essu_dev_some-refresh-token',
1985+
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
1986+
authentication: mockUser,
1987+
});
1988+
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
1989+
1990+
provider = new SAMLAuthenticationProvider(mockOptions, {
1991+
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
1992+
});
1993+
});
1994+
1995+
describe('`login` method', () => {
1996+
it('properly constructs ES native user profile activate grant when UIAM is enabled.', async () => {
1997+
const request = httpServerMock.createKibanaRequest();
1998+
await expect(
1999+
provider.login(
2000+
request,
2001+
{ type: SAMLLogin.LoginWithSAMLResponse, samlResponse: mockSAMLSet1.samlResponse },
2002+
{
2003+
requestIdMap: {
2004+
[mockSAMLSet1.requestId]: { redirectURL: '/test-base-path/some-path#some-app' },
2005+
},
2006+
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
2007+
}
2008+
)
2009+
).resolves.toEqual(
2010+
AuthenticationResult.redirectTo('/test-base-path/some-path#some-app', {
2011+
userProfileGrant: { type: 'accessToken', accessToken: 'x_essu_dev_some-token' },
2012+
state: {
2013+
accessToken: 'x_essu_dev_some-token',
2014+
refreshToken: 'x_essu_dev_some-refresh-token',
2015+
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
2016+
},
2017+
user: mockUser,
2018+
})
2019+
);
2020+
2021+
expect(mockOptions.uiam?.getUserProfileGrant).not.toHaveBeenCalled();
2022+
expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({
2023+
method: 'POST',
2024+
path: '/_security/saml/authenticate',
2025+
body: {
2026+
ids: [mockSAMLSet1.requestId],
2027+
content: mockSAMLSet1.samlResponse,
2028+
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
2029+
},
2030+
});
2031+
});
2032+
});
2033+
2034+
describe('`authenticate` method', () => {
2035+
it('properly constructs authentication headers only with ES native access token when UIAM is enabled.', async () => {
2036+
const request = httpServerMock.createKibanaRequest({ headers: {} });
2037+
const state = {
2038+
accessToken: 'x_essu_dev_some-valid-token',
2039+
refreshToken: 'x_essu_dev_some-valid-refresh-token',
2040+
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
2041+
};
2042+
const authorization = `Bearer ${state.accessToken}`;
2043+
2044+
await expect(provider.authenticate(request, state)).resolves.toEqual(
2045+
AuthenticationResult.succeeded(mockUser, { authHeaders: { authorization } })
2046+
);
2047+
2048+
expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } });
2049+
2050+
expect(request.headers).not.toHaveProperty('authorization');
2051+
expect(request.headers).not.toHaveProperty(ES_CLIENT_AUTHENTICATION_HEADER);
2052+
});
2053+
});
2054+
2055+
describe('`logout` method', () => {
2056+
it('returns `notHandled` if state is not presented or does not include access token.', async () => {
2057+
const request = httpServerMock.createKibanaRequest();
2058+
2059+
await expect(provider.logout(request)).resolves.toEqual(
2060+
DeauthenticationResult.notHandled()
2061+
);
2062+
2063+
expect(mockOptions.client.asInternalUser.transport.request).not.toHaveBeenCalled();
2064+
});
2065+
2066+
it('fails if token invalidation fails.', async () => {
2067+
const request = httpServerMock.createKibanaRequest();
2068+
const accessToken = 'x_essu_dev_x-saml-token';
2069+
const refreshToken = 'x_essu_dev_x-saml-refresh-token';
2070+
2071+
const failureReason = new errors.ResponseError(
2072+
securityMock.createApiResponse({ statusCode: 500, body: {} })
2073+
);
2074+
mockOptions.client.asInternalUser.transport.request.mockRejectedValue(failureReason);
2075+
2076+
await expect(
2077+
provider.logout(request, {
2078+
accessToken,
2079+
refreshToken,
2080+
realm: ELASTIC_CLOUD_SSO_REALM_NAME,
2081+
})
2082+
).resolves.toEqual(DeauthenticationResult.failed(failureReason));
2083+
2084+
expect(mockOptions.uiam?.invalidateSessionTokens).not.toHaveBeenCalled();
2085+
2086+
expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1);
2087+
expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({
2088+
method: 'POST',
2089+
path: '/_security/saml/logout',
2090+
body: { token: accessToken, refresh_token: refreshToken },
2091+
});
2092+
});
2093+
2094+
it('redirects to `loggedOut` URL.', async () => {
2095+
const request = httpServerMock.createKibanaRequest();
2096+
const accessToken = 'x_essu_dev_x-saml-token';
2097+
const refreshToken = 'x_essu_dev_x-saml-refresh-token';
2098+
2099+
mockOptions.client.asInternalUser.transport.request.mockResolvedValue({ redirect: null });
2100+
2101+
await expect(
2102+
provider.logout(request, {
2103+
accessToken,
2104+
refreshToken,
2105+
realm: 'test-realm',
2106+
})
2107+
).resolves.toEqual(DeauthenticationResult.redirectTo(mockOptions.urls.loggedOut(request)));
2108+
2109+
expect(mockOptions.uiam?.invalidateSessionTokens).not.toHaveBeenCalled();
2110+
2111+
expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledTimes(1);
2112+
expect(mockOptions.client.asInternalUser.transport.request).toHaveBeenCalledWith({
2113+
method: 'POST',
2114+
path: '/_security/saml/logout',
2115+
body: { token: accessToken, refresh_token: refreshToken },
2116+
});
2117+
});
2118+
});
2119+
2120+
describe('refresh token handling', () => {
2121+
it('succeeds if token from the state is expired, but has been successfully refreshed.', async () => {
2122+
const request = httpServerMock.createKibanaRequest();
2123+
const state = {
2124+
accessToken: 'x_essu_dev_expired-token',
2125+
refreshToken: 'x_essu_dev_valid-refresh-token',
2126+
realm: 'cloud-saml-kibana',
2127+
};
2128+
2129+
mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValueOnce(
2130+
new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} }))
2131+
);
2132+
2133+
mockOptions.tokens.refresh.mockResolvedValue({
2134+
accessToken: 'x_essu_dev_new-access-token',
2135+
refreshToken: 'x_essu_dev_new-refresh-token',
2136+
authenticationInfo: mockUser,
2137+
});
2138+
2139+
await expect(provider.authenticate(request, state)).resolves.toEqual(
2140+
AuthenticationResult.succeeded(mockUser, {
2141+
authHeaders: { authorization: 'Bearer x_essu_dev_new-access-token' },
2142+
state: {
2143+
accessToken: 'x_essu_dev_new-access-token',
2144+
refreshToken: 'x_essu_dev_new-refresh-token',
2145+
realm: 'cloud-saml-kibana',
2146+
},
2147+
})
2148+
);
2149+
2150+
expect(mockOptions.uiam?.refreshSessionTokens).not.toHaveBeenCalled();
2151+
2152+
expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1);
2153+
expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken);
2154+
2155+
expect(request.headers).not.toHaveProperty('authorization');
2156+
expect(request.headers).not.toHaveProperty(ES_CLIENT_AUTHENTICATION_HEADER);
2157+
});
2158+
2159+
it('fails if token from the state is expired, refresh attempt failed, and displays error from UIAM', async () => {
2160+
const request = httpServerMock.createKibanaRequest({ headers: {} });
2161+
const state = {
2162+
accessToken: 'x_essu_dev_expired-token',
2163+
refreshToken: 'x_essu_dev_invalid-refresh-token',
2164+
realm: 'cloud-saml-kibana',
2165+
};
2166+
const authorization = `Bearer ${state.accessToken}`;
2167+
2168+
mockScopedClusterClient.asCurrentUser.security.authenticate.mockRejectedValue(
2169+
new errors.ResponseError(securityMock.createApiResponse({ statusCode: 401, body: {} }))
2170+
);
2171+
2172+
const refreshFailureReason = new Boom.Boom('Authentication failed');
2173+
mockOptions.tokens.refresh.mockRejectedValue(refreshFailureReason);
2174+
2175+
await expect(provider.authenticate(request, state)).resolves.toEqual(
2176+
AuthenticationResult.failed(refreshFailureReason as any)
2177+
);
2178+
2179+
expect(mockOptions.uiam?.refreshSessionTokens).not.toHaveBeenCalled();
2180+
2181+
expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1);
2182+
expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(state.refreshToken);
2183+
2184+
expect(mockOptions.client.asScoped).toHaveBeenCalledWith({ headers: { authorization } });
2185+
2186+
expect(request.headers).not.toHaveProperty('authorization');
2187+
expect(request.headers).not.toHaveProperty(ES_CLIENT_AUTHENTICATION_HEADER);
19662188
});
19672189
});
19682190
});

0 commit comments

Comments
 (0)