Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
347 changes: 181 additions & 166 deletions x-pack/plugins/security/server/authentication/providers/saml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,193 +396,208 @@ describe('SAMLAuthenticationProvider', () => {
});
});

it('redirects to the home page if new SAML Response is for the same user.', async () => {
const request = httpServerMock.createKibanaRequest({ headers: {} });
const state = {
username: 'user',
accessToken: 'existing-valid-token',
refreshToken: 'existing-valid-refresh-token',
realm: 'test-realm',
};

const user = { username: 'user', authentication_realm: { name: 'saml1' } };
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
for (const [description, response] of [
[
'session is valid',
Promise.resolve({ username: 'user', authentication_realm: { name: 'saml1' } }),
],
[
'session is is expired',
Promise.reject(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())),
],
] as Array<[string, Promise<any>]>) {
it(`redirects to the home page if new SAML Response is for the same user if ${description}.`, async () => {
const request = httpServerMock.createKibanaRequest({ headers: {} });
const state = {
username: 'user',
accessToken: 'existing-token',
refreshToken: 'existing-refresh-token',
realm: 'test-realm',
};
const authorization = `Bearer ${state.accessToken}`;

mockOptions.client.callAsInternalUser.mockResolvedValue({
username: 'user',
access_token: 'new-valid-token',
refresh_token: 'new-valid-refresh-token',
realm: 'test-realm',
});
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response);
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);

mockOptions.tokens.invalidate.mockResolvedValue(undefined);
mockOptions.client.callAsInternalUser.mockResolvedValue({
username: 'user',
access_token: 'new-valid-token',
refresh_token: 'new-valid-refresh-token',
realm: 'test-realm',
});

mockOptions.tokens.invalidate.mockResolvedValue(undefined);

await expect(
provider.login(
request,
{ type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
state
)
).resolves.toEqual(
AuthenticationResult.redirectTo('/base-path/', {
state: {
username: 'user',
accessToken: 'new-valid-token',
refreshToken: 'new-valid-refresh-token',
realm: 'test-realm',
},
})
);

await expect(
provider.login(
request,
{ type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
state
)
).resolves.toEqual(
AuthenticationResult.redirectTo('/base-path/', {
state: {
username: 'user',
accessToken: 'new-valid-token',
refreshToken: 'new-valid-refresh-token',
realm: 'test-realm',
},
})
);
expectAuthenticateCall(mockOptions.client, { headers: { authorization } });

expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
'shield.samlAuthenticate',
{
body: { ids: [], content: 'saml-response-xml' },
}
);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
'shield.samlAuthenticate',
{
body: { ids: [], content: 'saml-response-xml' },
}
);

expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
});
});
});

it('redirects to `overwritten_session` if new SAML Response is for the another user.', async () => {
const request = httpServerMock.createKibanaRequest({ headers: {} });
const state = {
username: 'user',
accessToken: 'existing-valid-token',
refreshToken: 'existing-valid-refresh-token',
realm: 'test-realm',
};

const existingUser = { username: 'user', authentication_realm: { name: 'saml1' } };
const newUser = { username: 'new-user', authentication_realm: { name: 'saml1' } };
mockOptions.client.asScoped.mockImplementation(scopeableRequest => {
if (scopeableRequest?.headers.authorization === `Bearer ${state.accessToken}`) {
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser);
return mockScopedClusterClient;
}
it(`redirects to \`overwritten_session\` if new SAML Response is for the another user if ${description}.`, async () => {
const request = httpServerMock.createKibanaRequest({ headers: {} });
const state = {
username: 'user',
accessToken: 'existing-token',
refreshToken: 'existing-refresh-token',
realm: 'test-realm',
};
const authorization = `Bearer ${state.accessToken}`;

const newUser = { username: 'new-user', authentication_realm: { name: 'saml1' } };
mockOptions.client.asScoped.mockImplementation(scopeableRequest => {
if (scopeableRequest?.headers.authorization === `Bearer ${state.accessToken}`) {
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockImplementation(() => response);
return mockScopedClusterClient;
}

if (scopeableRequest?.headers.authorization === 'Bearer new-valid-token') {
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(newUser);
return mockScopedClusterClient;
}
if (scopeableRequest?.headers.authorization === 'Bearer new-valid-token') {
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(newUser);
return mockScopedClusterClient;
}

throw new Error('Unexpected call');
});
throw new Error('Unexpected call');
});

mockOptions.client.callAsInternalUser.mockResolvedValue({
username: 'new-user',
access_token: 'new-valid-token',
refresh_token: 'new-valid-refresh-token',
realm: 'test-realm',
});

mockOptions.tokens.invalidate.mockResolvedValue(undefined);
mockOptions.client.callAsInternalUser.mockResolvedValue({
username: 'new-user',
access_token: 'new-valid-token',
refresh_token: 'new-valid-refresh-token',
realm: 'test-realm',
});

mockOptions.tokens.invalidate.mockResolvedValue(undefined);

await expect(
provider.login(
request,
{ type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
state
)
).resolves.toEqual(
AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', {
state: {
username: 'new-user',
accessToken: 'new-valid-token',
refreshToken: 'new-valid-refresh-token',
realm: 'test-realm',
},
})
);

await expect(
provider.login(
request,
{ type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
state
)
).resolves.toEqual(
AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', {
state: {
username: 'new-user',
accessToken: 'new-valid-token',
refreshToken: 'new-valid-refresh-token',
realm: 'test-realm',
},
})
);
expectAuthenticateCall(mockOptions.client, { headers: { authorization } });

expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
'shield.samlAuthenticate',
{
body: { ids: [], content: 'saml-response-xml' },
}
);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
'shield.samlAuthenticate',
{
body: { ids: [], content: 'saml-response-xml' },
}
);

expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
});
});
});

it('redirects to `overwritten_session` if new SAML Response is for another realm.', async () => {
const request = httpServerMock.createKibanaRequest();
const state = {
username: 'user',
accessToken: 'existing-valid-token',
refreshToken: 'existing-valid-refresh-token',
realm: 'saml1',
};

const existingUser = { username: 'user', authentication_realm: { name: 'saml1' } };
const newUser = { username: 'user', authentication_realm: { name: 'saml2' } };
mockOptions.client.asScoped.mockImplementation(scopeableRequest => {
if (scopeableRequest?.headers.authorization === `Bearer ${state.accessToken}`) {
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser);
return mockScopedClusterClient;
}

if (scopeableRequest?.headers.authorization === 'Bearer new-valid-token') {
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(newUser);
return mockScopedClusterClient;
}

throw new Error('Unexpected call');
});
it(`redirects to \`overwritten_session\` if new SAML Response is for another realm if ${description}.`, async () => {
const request = httpServerMock.createKibanaRequest();
const state = {
username: 'user',
accessToken: 'existing-valid-token',
refreshToken: 'existing-valid-refresh-token',
realm: 'saml1',
};

const existingUser = { username: 'user', authentication_realm: { name: 'saml1' } };
const newUser = { username: 'user', authentication_realm: { name: 'saml2' } };
mockOptions.client.asScoped.mockImplementation(scopeableRequest => {
if (scopeableRequest?.headers.authorization === `Bearer ${state.accessToken}`) {
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(existingUser);
return mockScopedClusterClient;
}

mockOptions.client.callAsInternalUser.mockResolvedValue({
username: 'user',
access_token: 'new-valid-token',
refresh_token: 'new-valid-refresh-token',
realm: 'saml2',
});
if (scopeableRequest?.headers.authorization === 'Bearer new-valid-token') {
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(newUser);
return mockScopedClusterClient;
}

mockOptions.tokens.invalidate.mockResolvedValue(undefined);
throw new Error('Unexpected call');
});

await expect(
provider.login(
request,
{ type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
state
)
).resolves.toEqual(
AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', {
state: {
username: 'user',
accessToken: 'new-valid-token',
refreshToken: 'new-valid-refresh-token',
realm: 'saml2',
},
})
);
mockOptions.client.callAsInternalUser.mockResolvedValue({
username: 'user',
access_token: 'new-valid-token',
refresh_token: 'new-valid-refresh-token',
realm: 'saml2',
});

mockOptions.tokens.invalidate.mockResolvedValue(undefined);

await expect(
provider.login(
request,
{ type: SAMLLogin.LoginWithSAMLResponse, samlResponse: 'saml-response-xml' },
state
)
).resolves.toEqual(
AuthenticationResult.redirectTo('/mock-server-basepath/security/overwritten_session', {
state: {
username: 'user',
accessToken: 'new-valid-token',
refreshToken: 'new-valid-refresh-token',
realm: 'saml2',
},
})
);

expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
'shield.samlAuthenticate',
{
body: { ids: [], content: 'saml-response-xml' },
}
);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith(
'shield.samlAuthenticate',
{
body: { ids: [], content: 'saml-response-xml' },
}
);

expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
});
});
});
}
});

describe('User initiated login with captured redirect URL', () => {
Expand Down
12 changes: 8 additions & 4 deletions x-pack/plugins/security/server/authentication/providers/saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,14 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
return await this.loginWithSAMLResponse(request, samlResponse, state);
}

if (authenticationResult.succeeded()) {
// If user has been authenticated via session, but request also includes SAML payload
// we should check whether this payload is for the exactly same user and if not
// we'll re-authenticate user and forward to a page with the respective warning.
// If user has been authenticated via session or failed to do so because of expired access token,
// but request also includes SAML payload we should check whether this payload is for the exactly
// same user and if not we'll re-authenticate user and forward to a page with the respective warning.
if (
authenticationResult.succeeded() ||
(authenticationResult.failed() &&
Tokens.isAccessTokenExpiredError(authenticationResult.error))
) {
return await this.loginWithNewSAMLResponse(
request,
samlResponse,
Expand Down
Loading