mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
feat(core): add the saml assertion handle flow (#4872)
* feat(core): add saml assertion handle flow add saml assertion handle flow * fix(core): address some comments address some comments * chore(core): comments update comments update * chore(core): add deprecated tag add dreprecated tag * refactor(core): use url object instead of plain string use url object instead of plain string
This commit is contained in:
parent
733261af55
commit
edb2bdccec
11 changed files with 359 additions and 67 deletions
|
@ -6,12 +6,17 @@ import { z } from 'zod';
|
|||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { verifyBearerTokenFromRequest } from '#src/middleware/koa-auth/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import SamlConnector from '#src/sso/SamlConnector/index.js';
|
||||
import { ssoConnectorFactories } from '#src/sso/index.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
import {
|
||||
getConnectorSessionResultFromJti,
|
||||
assignConnectorSessionResultViaJti,
|
||||
getSingleSignOnSessionResultByJti,
|
||||
assignSamlAssertionResultViaJti,
|
||||
} from '#src/utils/saml-assertion-handler.js';
|
||||
|
||||
import { ssoPath } from './interaction/const.js';
|
||||
import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
||||
|
||||
/**
|
||||
|
@ -19,11 +24,12 @@ import type { AnonymousRouter, RouterInitArgs } from './types.js';
|
|||
* This router will have a route `/authn` to authenticate tokens with a general manner.
|
||||
*/
|
||||
export default function authnRoutes<T extends AnonymousRouter>(
|
||||
...[router, { envSet, provider, libraries }]: RouterInitArgs<T>
|
||||
...[router, { envSet, provider, libraries, id: tenantId }]: RouterInitArgs<T>
|
||||
) {
|
||||
const {
|
||||
users: { findUserRoles },
|
||||
socials: { getConnector },
|
||||
ssoConnectors: ssoConnectorsLibrary,
|
||||
} = libraries;
|
||||
|
||||
const hasuraResponseGuard = z.object({
|
||||
|
@ -90,7 +96,11 @@ export default function authnRoutes<T extends AnonymousRouter>(
|
|||
}
|
||||
);
|
||||
|
||||
// Create an specialized API to handle SAML assertion
|
||||
/**
|
||||
* Standard SAML social connector's assertion consumer service endpoint
|
||||
* @deprecated
|
||||
* Will be replaced by the SSO SAML assertion consumer service endpoint bellow.
|
||||
*/
|
||||
router.post(
|
||||
'/authn/saml/:connectorId',
|
||||
/**
|
||||
|
@ -144,4 +154,74 @@ export default function authnRoutes<T extends AnonymousRouter>(
|
|||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* SAML SSO connector's assertion consumer service endpoint
|
||||
*
|
||||
* @param connectorId The connector id.
|
||||
* @property body The SAML assertion response body.
|
||||
* @property body.RelayState We use this to find the connector session.
|
||||
* RelayState is a SAML standard parameter that will be transmitted between the identity provider and the service provider.
|
||||
* @returns Redirect to the redirect uri find in the connector session storage.
|
||||
*
|
||||
* @remark
|
||||
* This API is used to handle SSO SAML assertion from the identity provider.
|
||||
* Validate and parse the SAML assertion, then store the assertion data to the connector session storage.
|
||||
* Redirect to the redirect uri find in the connector session storage.
|
||||
*/
|
||||
router.post(
|
||||
`/authn/${ssoPath}/saml/:connectorId`,
|
||||
koaGuard({
|
||||
body: z.object({ RelayState: z.string() }).catchall(z.unknown()),
|
||||
params: z.object({ connectorId: z.string().min(1) }),
|
||||
status: [302, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { connectorId },
|
||||
body,
|
||||
} = ctx.guard;
|
||||
|
||||
// Will throw 404 if connector not found, or not supported
|
||||
const connectorData = await ssoConnectorsLibrary.getSsoConnectorById(connectorId);
|
||||
const { providerName } = connectorData;
|
||||
|
||||
// Get relay state from the request body. We need to use it to find the connector session.
|
||||
// All the rest of the request body will be validated and parsed by the connector.
|
||||
const { RelayState: jti } = body;
|
||||
|
||||
// Retrieve the single sign on session data using the jti
|
||||
const singleSignOnSession = await getSingleSignOnSessionResultByJti(jti, provider);
|
||||
|
||||
const { redirectUri, state, connectorId: sessionConnectorId } = singleSignOnSession;
|
||||
|
||||
assertThat(
|
||||
connectorId === sessionConnectorId,
|
||||
'session.connector_validation_session_not_found'
|
||||
);
|
||||
|
||||
// Will throw ConnectorError if the config is invalid
|
||||
const connectorInstance = new ssoConnectorFactories[providerName].constructor(
|
||||
connectorData,
|
||||
tenantId
|
||||
);
|
||||
|
||||
assertThat(connectorInstance instanceof SamlConnector, 'connector.unexpected_type');
|
||||
|
||||
const userInfo = await connectorInstance.parseSamlAssertion(body);
|
||||
|
||||
await assignSamlAssertionResultViaJti(jti, provider, {
|
||||
...singleSignOnSession,
|
||||
userInfo,
|
||||
});
|
||||
|
||||
// Client side will verify the state to prevent CSRF attack.
|
||||
const url = new URL(redirectUri);
|
||||
url.searchParams.append('state', state);
|
||||
|
||||
ctx.redirect(url.toString());
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -52,11 +52,6 @@ export const verifySsoOnlyEmailIdentifier = async (
|
|||
/**
|
||||
* Get the single sign on session data from the oidc provider session storage.
|
||||
*
|
||||
* @param ctx
|
||||
* @param provider
|
||||
* @param connectorId
|
||||
* @returns The single sign on session data
|
||||
*
|
||||
* @remark Forked from ./social-verification.ts.
|
||||
* Use SingleSignOnSession guard instead of ConnectorSession guard.
|
||||
*/
|
||||
|
|
|
@ -3,6 +3,7 @@ import { createMockUtils } from '@logto/shared/esm';
|
|||
import type Provider from 'oidc-provider';
|
||||
|
||||
import { mockSsoConnector, wellConfiguredSsoConnector } from '#src/__mocks__/sso.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import { type WithLogContext } from '#src/middleware/koa-audit-log.js';
|
||||
import { OidcSsoConnector } from '#src/sso/OidcSsoConnector/index.js';
|
||||
import { ssoConnectorFactories } from '#src/sso/index.js';
|
||||
|
@ -27,6 +28,7 @@ const findUserByEmailMock = jest.fn();
|
|||
const insertUserMock = jest.fn();
|
||||
const storeInteractionResultMock = jest.fn();
|
||||
const getAvailableSsoConnectorsMock = jest.fn();
|
||||
const getSingleSignOnSessionResultMock = jest.fn();
|
||||
|
||||
class MockOidcSsoConnector extends OidcSsoConnector {
|
||||
override getAuthorizationUrl = getAuthorizationUrlMock;
|
||||
|
@ -38,6 +40,10 @@ mockEsm('./interaction.js', () => ({
|
|||
storeInteractionResult: storeInteractionResultMock,
|
||||
}));
|
||||
|
||||
mockEsm('./single-sign-on-guard.js', () => ({
|
||||
getSingleSignOnSessionResult: getSingleSignOnSessionResultMock,
|
||||
}));
|
||||
|
||||
jest
|
||||
.spyOn(ssoConnectorFactories.OIDC, 'constructor')
|
||||
.mockImplementation((data: SsoConnector) => new MockOidcSsoConnector(data));
|
||||
|
@ -109,6 +115,20 @@ describe('Single sign on util methods tests', () => {
|
|||
});
|
||||
|
||||
describe('getSsoAuthentication tests', () => {
|
||||
it('should throw an error if the connector config is invalid', async () => {
|
||||
getSingleSignOnSessionResultMock.mockRejectedValueOnce(
|
||||
new RequestError('session.connector_validation_session_not_found')
|
||||
);
|
||||
|
||||
await expect(getSsoAuthentication(mockContext, tenant, mockSsoConnector, {})).rejects.toThrow(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
expect.objectContaining({
|
||||
status: 400,
|
||||
code: `session.connector_validation_session_not_found`,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the connector config is invalid', async () => {
|
||||
await expect(getSsoAuthentication(mockContext, tenant, mockSsoConnector, {})).rejects.toThrow(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
|
@ -117,6 +137,19 @@ describe('Single sign on util methods tests', () => {
|
|||
});
|
||||
|
||||
it('should return the authentication result', async () => {
|
||||
const session = {
|
||||
connectorId: 'connectorId',
|
||||
jti: 'jti',
|
||||
redirectUri: 'https://example.com',
|
||||
state: 'state',
|
||||
};
|
||||
|
||||
const payload = {
|
||||
code: 'code',
|
||||
};
|
||||
|
||||
getSingleSignOnSessionResultMock.mockResolvedValueOnce(session);
|
||||
|
||||
getUserInfoMock.mockResolvedValueOnce({ id: 'id', email: 'email' });
|
||||
getIssuerMock.mockReturnValueOnce('https://example.com');
|
||||
|
||||
|
@ -124,11 +157,11 @@ describe('Single sign on util methods tests', () => {
|
|||
mockContext,
|
||||
tenant,
|
||||
wellConfiguredSsoConnector,
|
||||
{}
|
||||
payload
|
||||
);
|
||||
|
||||
expect(getIssuerMock).toBeCalled();
|
||||
expect(getUserInfoMock).toBeCalled();
|
||||
expect(getUserInfoMock).toBeCalledWith(session, payload);
|
||||
|
||||
expect(result).toStrictEqual({
|
||||
issuer: 'https://example.com',
|
||||
|
|
|
@ -90,6 +90,8 @@ export const getSsoAuthentication = async (
|
|||
const log = createLog(`Interaction.SignIn.Identifier.SingleSignOn.Submit`);
|
||||
log.append({ connectorId, data });
|
||||
|
||||
const singleSignOnSession = await getSingleSignOnSessionResult(ctx, provider);
|
||||
|
||||
try {
|
||||
// Will throw ConnectorError if the config is invalid
|
||||
const connectorInstance = new ssoConnectorFactories[providerName].constructor(
|
||||
|
@ -98,9 +100,7 @@ export const getSsoAuthentication = async (
|
|||
);
|
||||
|
||||
const issuer = await connectorInstance.getIssuer();
|
||||
const userInfo = await connectorInstance.getUserInfo(data, async () =>
|
||||
getSingleSignOnSessionResult(ctx, provider)
|
||||
);
|
||||
const userInfo = await connectorInstance.getUserInfo(singleSignOnSession, data);
|
||||
|
||||
const result = {
|
||||
issuer,
|
||||
|
|
|
@ -4,7 +4,10 @@ import { assert, conditional } from '@silverhand/essentials';
|
|||
import snakecaseKeys from 'snakecase-keys';
|
||||
|
||||
import { type BaseOidcConfig, type BasicOidcConnectorConfig } from '../types/oidc.js';
|
||||
import { type CreateSingleSignOnSession, type GetSingleSignOnSession } from '../types/session.js';
|
||||
import {
|
||||
type SingleSignOnConnectorSession,
|
||||
type CreateSingleSignOnSession,
|
||||
} from '../types/session.js';
|
||||
|
||||
import { fetchOidcConfig, fetchToken, getIdTokenClaims } from './utils.js';
|
||||
|
||||
|
@ -91,21 +94,14 @@ class OidcConnector {
|
|||
* Handle the sign-in callback from the OIDC provider and return the user info
|
||||
*
|
||||
* @param data unknown oidc authorization response
|
||||
* @param getSession Get the connector session data from the oidc provider session storage. @see @logto/connector-kit
|
||||
* @param connectorSession The connector session data from the oidc provider session storage
|
||||
* @returns The user info from the OIDC provider
|
||||
* @remark Forked from @logto/oidc-connector
|
||||
*
|
||||
*/
|
||||
getUserInfo = async (data: unknown, getSession: GetSingleSignOnSession) => {
|
||||
assert(
|
||||
getSession,
|
||||
new ConnectorError(ConnectorErrorCodes.NotImplemented, {
|
||||
message: 'Connector session storage not found',
|
||||
})
|
||||
);
|
||||
|
||||
getUserInfo = async (connectorSession: SingleSignOnConnectorSession, data: unknown) => {
|
||||
const oidcConfig = await this.getOidcConfig();
|
||||
const { nonce, redirectUri } = await getSession();
|
||||
const { nonce, redirectUri } = connectorSession;
|
||||
|
||||
// Fetch token from the OIDC provider using authorization code
|
||||
const { idToken } = await fetchToken(oidcConfig, data, redirectUri);
|
||||
|
|
|
@ -4,8 +4,14 @@ import * as saml from 'samlify';
|
|||
import { z } from 'zod';
|
||||
|
||||
import { EnvSet, getTenantEndpoint } from '#src/env-set/index.js';
|
||||
import { ssoPath } from '#src/routes/interaction/const.js';
|
||||
|
||||
import { type SamlConfig, type SamlConnectorConfig, samlMetadataGuard } from '../types/saml.js';
|
||||
import {
|
||||
type SamlConfig,
|
||||
type SamlConnectorConfig,
|
||||
samlMetadataGuard,
|
||||
type ExtendedSocialUserInfo,
|
||||
} from '../types/saml.js';
|
||||
|
||||
import {
|
||||
parseXmlMetadata,
|
||||
|
@ -23,7 +29,7 @@ import {
|
|||
* All the SAML single sign-on connector should extend this class.
|
||||
*
|
||||
* @property config The SAML connector config
|
||||
* @property acsUrl The SAML connector's assertion consumer service URL
|
||||
* @property acsUrl The SAML connector's assertion consumer service URL {@link file://src/routes/authn.ts}
|
||||
* @property _rawSamlMetadata The cached raw SAML metadata (in XML-format) from the raw SAML SSO connector config
|
||||
* @property _parsedSamlMetadata The cached parsed SAML metadata from the raw SAML SSO connector config
|
||||
* @property _samlAssertionContent The cached parsed SAML assertion from IdP (with attribute mapping applied)
|
||||
|
@ -45,8 +51,7 @@ class SamlConnector {
|
|||
) {
|
||||
this.acsUrl = appendPath(
|
||||
getTenantEndpoint(tenantId, EnvSet.values),
|
||||
// TODO: update this endpoint
|
||||
`api/authn/saml/sso/${ssoConnectorId}`
|
||||
`api/authn/${ssoPath}/saml/${ssoConnectorId}`
|
||||
).toString();
|
||||
}
|
||||
|
||||
|
@ -93,7 +98,7 @@ class SamlConnector {
|
|||
*
|
||||
* @returns The parsed SAML assertion from IdP (with attribute mapping applied).
|
||||
*/
|
||||
async parseSamlAssertion(assertion: Record<string, unknown>) {
|
||||
async parseSamlAssertion(assertion: Record<string, unknown>): Promise<ExtendedSocialUserInfo> {
|
||||
const parsedConfig = await this.getSamlConfig();
|
||||
const profileMap = attributeMappingPostProcessor(parsedConfig.attributeMapping);
|
||||
const idpMetadataXml = await this.getIdpXmlMetadata();
|
||||
|
@ -149,16 +154,15 @@ class SamlConnector {
|
|||
|
||||
// eslint-disable-next-line new-cap
|
||||
const identityProvider = saml.IdentityProvider({
|
||||
wantAuthnRequestsSigned: true, // Sign auth request by default
|
||||
metadata: idpMetadataXml,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
const serviceProvider = saml.ServiceProvider({
|
||||
entityID,
|
||||
entityID, // FIXME: @darcyYe
|
||||
relayState,
|
||||
nameIDFormat: nameIdFormat,
|
||||
signingCert: x509Certificate,
|
||||
authnRequestsSigned: true, // Sign auth request by default
|
||||
requestSignatureAlgorithm: signingAlgorithm,
|
||||
assertionConsumerService: [
|
||||
{
|
||||
|
|
|
@ -2,12 +2,16 @@ import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
|
|||
import { type SsoConnector } from '@logto/schemas';
|
||||
|
||||
import { SsoProviderName } from '#src/sso/types/index.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import SamlConnector from '../SamlConnector/index.js';
|
||||
import { type SingleSignOnFactory } from '../index.js';
|
||||
import { type SingleSignOn } from '../types/index.js';
|
||||
import { samlConnectorConfigGuard } from '../types/saml.js';
|
||||
import { type CreateSingleSignOnSession } from '../types/session.js';
|
||||
import {
|
||||
type SingleSignOnConnectorSession,
|
||||
type CreateSingleSignOnSession,
|
||||
} from '../types/session.js';
|
||||
|
||||
/**
|
||||
* SAML SSO connector
|
||||
|
@ -75,12 +79,16 @@ export class SamlSsoConnector extends SamlConnector implements SingleSignOn {
|
|||
/**
|
||||
* Get social user info.
|
||||
*
|
||||
* @param assertion The SAML assertion from IdP.
|
||||
*
|
||||
* @param connectorSession The connector session data from interaction session storage.
|
||||
* @returns The social user info extracted from SAML assertion.
|
||||
*
|
||||
* @remarks For SAML connector, userInfo will be extracted from the SAML assertion by ACS callback endpoint.
|
||||
* This method only asserts the userInfo is not null and directly return it.
|
||||
*/
|
||||
async getUserInfo(assertion: Record<string, unknown>) {
|
||||
return this.parseSamlAssertion(assertion);
|
||||
async getUserInfo({ userInfo }: SingleSignOnConnectorSession) {
|
||||
assertThat(userInfo, 'session.connector_validation_session_not_found');
|
||||
|
||||
return userInfo;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,11 @@ import {
|
|||
getInteractionFromProviderByJti,
|
||||
assignResultToInteraction,
|
||||
} from '#src/routes/interaction/utils/interaction.js';
|
||||
import {
|
||||
type SingleSignOnConnectorSession,
|
||||
singleSignOnConnectorSessionGuard,
|
||||
} from '#src/sso/types/index.js';
|
||||
import { type ExtendedSocialUserInfo } from '#src/sso/types/saml.js';
|
||||
|
||||
import assertThat from './assert-that.js';
|
||||
|
||||
|
@ -39,6 +44,11 @@ export const assignConnectorSessionResultViaJti = async (
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Used by the standard SAML social connectors ACS endpoint.
|
||||
* @deprecated
|
||||
* Will be cleaned once the old SAML social connectors are removed.
|
||||
*/
|
||||
export const getConnectorSessionResultFromJti = async (
|
||||
jti: string,
|
||||
provider: Provider
|
||||
|
@ -60,3 +70,51 @@ export const getConnectorSessionResultFromJti = async (
|
|||
|
||||
return connectorSessionResult.data.connectorSession;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the single sign on session data from the oidc provider session storage by the jti.
|
||||
*
|
||||
* @param jti The jti of the interaction session.
|
||||
*
|
||||
* @remark This method is used by the SSO SAML assertion consumer service endpoint.
|
||||
* Since we do not have the interaction ctx under SAML ACS endpoint, we need to get the session data by the jti.
|
||||
* Forked from the above {@link getConnectorSessionResultFromJti} method, with more detailed SingleSignOnConnectorSession type guard.
|
||||
*/
|
||||
export const getSingleSignOnSessionResultByJti = async (
|
||||
jti: string,
|
||||
provider: Provider
|
||||
): Promise<SingleSignOnConnectorSession> => {
|
||||
const { result } = await getInteractionFromProviderByJti(jti, provider);
|
||||
|
||||
const singleSignOnSessionResult = z
|
||||
.object({
|
||||
connectorSession: singleSignOnConnectorSessionGuard,
|
||||
})
|
||||
.safeParse(result);
|
||||
|
||||
assertThat(singleSignOnSessionResult.success, 'session.connector_validation_session_not_found');
|
||||
|
||||
return singleSignOnSessionResult.data.connectorSession;
|
||||
};
|
||||
|
||||
/**
|
||||
* Assign the SAML assertion result to the interaction single sign-on session storage by the jti.
|
||||
*
|
||||
* @param jti The jti of the interaction session.
|
||||
*/
|
||||
export const assignSamlAssertionResultViaJti = async (
|
||||
jti: string,
|
||||
provider: Provider,
|
||||
sessionResultWithAssertion: Omit<SingleSignOnConnectorSession, 'userInfo'> & {
|
||||
userInfo: ExtendedSocialUserInfo;
|
||||
}
|
||||
) => {
|
||||
const interaction = await getInteractionFromProviderByJti(jti, provider);
|
||||
|
||||
const { result } = interaction;
|
||||
|
||||
await assignResultToInteraction(interaction, {
|
||||
...result,
|
||||
connectorSession: sessionResultWithAssertion,
|
||||
});
|
||||
};
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -35,3 +35,16 @@ export const getSsoConnectorsByEmail = async (
|
|||
})
|
||||
.json<string[]>();
|
||||
};
|
||||
|
||||
export const postSamlAssertion = async (data: {
|
||||
connectorId: string;
|
||||
RelayState: string;
|
||||
SAMLResponse: string;
|
||||
}) => {
|
||||
const { connectorId, ...payload } = data;
|
||||
return api
|
||||
.post(`authn/${ssoPath}/saml/${connectorId}`, {
|
||||
json: payload,
|
||||
})
|
||||
.json();
|
||||
};
|
||||
|
|
|
@ -1,55 +1,116 @@
|
|||
import { InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import { getSsoAuthorizationUrl } from '#src/api/interaction-sso.js';
|
||||
import {
|
||||
partialConfigAndProviderNames,
|
||||
samlAssertion,
|
||||
} from '#src/__mocks__/sso-connectors-mock.js';
|
||||
import { getSsoAuthorizationUrl, postSamlAssertion } from '#src/api/interaction-sso.js';
|
||||
import { putInteraction } from '#src/api/interaction.js';
|
||||
import { createSsoConnector, deleteSsoConnectorById } from '#src/api/sso-connector.js';
|
||||
import { SsoProviderName } from '#src/constants.js';
|
||||
import { initClient } from '#src/helpers/client.js';
|
||||
import { expectRejects } from '#src/helpers/index.js';
|
||||
|
||||
describe('Single Sign On Sad Path', () => {
|
||||
const state = 'foo_state';
|
||||
const redirectUri = 'http://foo.dev/callback';
|
||||
|
||||
it('should throw if connector not found', async () => {
|
||||
const client = await initClient();
|
||||
describe('getAuthorizationUrl', () => {
|
||||
it('should throw if connector not found', async () => {
|
||||
const client = await initClient();
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.send(getSsoAuthorizationUrl, {
|
||||
connectorId: 'foo',
|
||||
state,
|
||||
redirectUri,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.send(getSsoAuthorizationUrl, {
|
||||
connectorId: 'foo',
|
||||
state,
|
||||
redirectUri,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
it('should throw if connector config is invalid', async () => {
|
||||
const { id } = await createSsoConnector({
|
||||
providerName: SsoProviderName.OIDC,
|
||||
connectorName: 'test-oidc',
|
||||
config: {
|
||||
clientId: 'foo',
|
||||
clientSecret: 'bar',
|
||||
},
|
||||
});
|
||||
|
||||
const client = await initClient();
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.send(getSsoAuthorizationUrl, {
|
||||
connectorId: id,
|
||||
state,
|
||||
redirectUri,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
|
||||
await deleteSsoConnectorById(id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if connector config is invalid', async () => {
|
||||
const { id } = await createSsoConnector({
|
||||
providerName: SsoProviderName.OIDC,
|
||||
connectorName: 'test-oidc',
|
||||
config: {
|
||||
clientId: 'foo',
|
||||
clientSecret: 'bar',
|
||||
},
|
||||
describe('postSamlAssertion', () => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-let
|
||||
let connectorId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const mockSamlConnector = partialConfigAndProviderNames[1]!;
|
||||
|
||||
const { id } = await createSsoConnector({
|
||||
connectorName: 'test-saml',
|
||||
...mockSamlConnector,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
connectorId = id;
|
||||
});
|
||||
|
||||
const client = await initClient();
|
||||
|
||||
await client.successSend(putInteraction, {
|
||||
event: InteractionEvent.SignIn,
|
||||
afterAll(async () => {
|
||||
await deleteSsoConnectorById(connectorId);
|
||||
});
|
||||
|
||||
await expect(
|
||||
client.send(getSsoAuthorizationUrl, {
|
||||
connectorId: id,
|
||||
it('should throw if the session dose not exist', async () => {
|
||||
await initClient();
|
||||
|
||||
await expectRejects(
|
||||
postSamlAssertion({ connectorId, RelayState: 'foo', SAMLResponse: samlAssertion }),
|
||||
{
|
||||
code: 'session.not_found',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if the response is invalid', async () => {
|
||||
const client = await initClient();
|
||||
|
||||
const { redirectTo } = await client.send(getSsoAuthorizationUrl, {
|
||||
connectorId,
|
||||
state,
|
||||
redirectUri,
|
||||
})
|
||||
).rejects.toThrow();
|
||||
redirectUri: 'http://foo.dev/callback',
|
||||
});
|
||||
|
||||
await deleteSsoConnectorById(id);
|
||||
const url = new URL(redirectTo);
|
||||
const RelayState = url.searchParams.get('RelayState')!;
|
||||
|
||||
await expectRejects(
|
||||
postSamlAssertion({ connectorId, RelayState, SAMLResponse: samlAssertion }),
|
||||
{
|
||||
code: 'connector.general',
|
||||
statusCode: 400,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue