Skip to content

Commit

Permalink
[7.7] Allow IdP initiated SAML login with session containing expired …
Browse files Browse the repository at this point in the history
…token. (#64339)
  • Loading branch information
azasypkin authored May 15, 2020
1 parent 81df9ab commit 7827fa5
Show file tree
Hide file tree
Showing 5 changed files with 387 additions and 308 deletions.
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

0 comments on commit 7827fa5

Please sign in to comment.