diff --git a/packages/core/src/routes/authn.ts b/packages/core/src/routes/authn.ts index 6699e31a7..a93fe50de 100644 --- a/packages/core/src/routes/authn.ts +++ b/packages/core/src/routes/authn.ts @@ -6,6 +6,8 @@ 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 { ssoConnectorFactories } from '#src/sso/index.js'; +import { SsoProviderName } from '#src/sso/types/index.js'; import assertThat from '#src/utils/assert-that.js'; import { getConnectorSessionResultFromJti, @@ -19,11 +21,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( - ...[router, { envSet, provider, libraries }]: RouterInitArgs + ...[router, { id: tenantId, envSet, provider, libraries }]: RouterInitArgs ) { const { users: { findUserRoles }, socials: { getConnector }, + ssoConnector: { getSsoConnectorById }, } = libraries; const hasuraResponseGuard = z.object({ @@ -144,4 +147,57 @@ export default function authnRoutes( return next(); } ); + + // TODO: refactor this, this SAML API for SSO is quite similar to the one for normal social sign-in, most of the logics can be reused. + router.post( + '/authn/saml/sso/:ssoConnectorId', + /** + * The API does not care the type of the SAML assertion request body, simply pass this to + * SSO connector's built-in methods. + */ + koaGuard({ + body: jsonObjectGuard, + params: z.object({ ssoConnectorId: z.string().min(1) }), + status: 302, + }), + async (ctx, next) => { + const { + params: { ssoConnectorId }, + body, + } = ctx.guard; + const ssoConnector = await getSsoConnectorById(ssoConnectorId); + + const samlAssertionGuard = z.object({ SAMLResponse: z.string(), RelayState: z.string() }); + const samlAssertionParseResult = samlAssertionGuard.safeParse(body); + + if (!samlAssertionParseResult.success) { + throw new ConnectorError( + ConnectorErrorCodes.InvalidResponse, + samlAssertionParseResult.error + ); + } + + /** + * Since `RelayState` will be returned with value unchanged, we use it to pass `jti` + * to find the connector session we used to store essential information. + */ + const { RelayState: jti } = samlAssertionParseResult.data; + + const getSession = async () => getConnectorSessionResultFromJti(jti, provider); + const setSession = async (connectorSession: ConnectorSession) => + assignConnectorSessionResultViaJti(jti, provider, connectorSession); + + if (ssoConnector.providerName !== SsoProviderName.SAML) { + throw new RequestError({ code: 'sso_connector.saml_only' }); + } + + const { constructor } = ssoConnectorFactories[ssoConnector.providerName]; + const { validateSamlAssertion } = new constructor(ssoConnector, tenantId); + const redirectTo = await validateSamlAssertion({ body }, getSession, setSession); + + ctx.redirect(redirectTo); + + return next(); + } + ); } diff --git a/packages/phrases/src/locales/de/errors/index.ts b/packages/phrases/src/locales/de/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/de/errors/index.ts +++ b/packages/phrases/src/locales/de/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/de/errors/sso-connector.ts b/packages/phrases/src/locales/de/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/de/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/en/errors/index.ts b/packages/phrases/src/locales/en/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/en/errors/index.ts +++ b/packages/phrases/src/locales/en/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/en/errors/sso-connector.ts b/packages/phrases/src/locales/en/errors/sso-connector.ts new file mode 100644 index 000000000..6f82279b0 --- /dev/null +++ b/packages/phrases/src/locales/en/errors/sso-connector.ts @@ -0,0 +1,5 @@ +const sso_connector = { + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/es/errors/index.ts b/packages/phrases/src/locales/es/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/es/errors/index.ts +++ b/packages/phrases/src/locales/es/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/es/errors/sso-connector.ts b/packages/phrases/src/locales/es/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/es/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/fr/errors/index.ts b/packages/phrases/src/locales/fr/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/fr/errors/index.ts +++ b/packages/phrases/src/locales/fr/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/fr/errors/sso-connector.ts b/packages/phrases/src/locales/fr/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/fr/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/it/errors/index.ts b/packages/phrases/src/locales/it/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/it/errors/index.ts +++ b/packages/phrases/src/locales/it/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/it/errors/sso-connector.ts b/packages/phrases/src/locales/it/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/it/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/ja/errors/index.ts b/packages/phrases/src/locales/ja/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/ja/errors/index.ts +++ b/packages/phrases/src/locales/ja/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/ja/errors/sso-connector.ts b/packages/phrases/src/locales/ja/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/ja/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/ko/errors/index.ts b/packages/phrases/src/locales/ko/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/ko/errors/index.ts +++ b/packages/phrases/src/locales/ko/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/ko/errors/sso-connector.ts b/packages/phrases/src/locales/ko/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/ko/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/pl-pl/errors/index.ts b/packages/phrases/src/locales/pl-pl/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/pl-pl/errors/index.ts +++ b/packages/phrases/src/locales/pl-pl/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/pl-pl/errors/sso-connector.ts b/packages/phrases/src/locales/pl-pl/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/pl-pl/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/pt-br/errors/index.ts b/packages/phrases/src/locales/pt-br/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/pt-br/errors/index.ts +++ b/packages/phrases/src/locales/pt-br/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/pt-br/errors/sso-connector.ts b/packages/phrases/src/locales/pt-br/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/pt-br/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/pt-pt/errors/index.ts b/packages/phrases/src/locales/pt-pt/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/pt-pt/errors/index.ts +++ b/packages/phrases/src/locales/pt-pt/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/pt-pt/errors/sso-connector.ts b/packages/phrases/src/locales/pt-pt/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/pt-pt/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/ru/errors/index.ts b/packages/phrases/src/locales/ru/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/ru/errors/index.ts +++ b/packages/phrases/src/locales/ru/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/ru/errors/sso-connector.ts b/packages/phrases/src/locales/ru/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/ru/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/tr-tr/errors/index.ts b/packages/phrases/src/locales/tr-tr/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/tr-tr/errors/index.ts +++ b/packages/phrases/src/locales/tr-tr/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/tr-tr/errors/sso-connector.ts b/packages/phrases/src/locales/tr-tr/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/tr-tr/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/zh-cn/errors/index.ts b/packages/phrases/src/locales/zh-cn/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/zh-cn/errors/index.ts +++ b/packages/phrases/src/locales/zh-cn/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/zh-cn/errors/sso-connector.ts b/packages/phrases/src/locales/zh-cn/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/zh-cn/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/zh-hk/errors/index.ts b/packages/phrases/src/locales/zh-hk/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/zh-hk/errors/index.ts +++ b/packages/phrases/src/locales/zh-hk/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/zh-hk/errors/sso-connector.ts b/packages/phrases/src/locales/zh-hk/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/zh-hk/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector); diff --git a/packages/phrases/src/locales/zh-tw/errors/index.ts b/packages/phrases/src/locales/zh-tw/errors/index.ts index acf9eda5f..67d11bdf3 100644 --- a/packages/phrases/src/locales/zh-tw/errors/index.ts +++ b/packages/phrases/src/locales/zh-tw/errors/index.ts @@ -16,6 +16,7 @@ import role from './role.js'; import scope from './scope.js'; import session from './session.js'; import sign_in_experiences from './sign-in-experiences.js'; +import sso_connector from './sso-connector.js'; import storage from './storage.js'; import subscription from './subscription.js'; import swagger from './swagger.js'; @@ -46,6 +47,7 @@ const errors = { subscription, application, organization, + sso_connector, }; export default Object.freeze(errors); diff --git a/packages/phrases/src/locales/zh-tw/errors/sso-connector.ts b/packages/phrases/src/locales/zh-tw/errors/sso-connector.ts new file mode 100644 index 000000000..4913c92be --- /dev/null +++ b/packages/phrases/src/locales/zh-tw/errors/sso-connector.ts @@ -0,0 +1,6 @@ +const sso_connector = { + /** UNTRANSLATED */ + saml_only: 'The endpoint only applies to SAML SSO connectors.', +}; + +export default Object.freeze(sso_connector);