Skip to content
This repository has been archived by the owner on Apr 3, 2023. It is now read-only.

Commit

Permalink
feat!: remove implicit flow and hybrid flow to ensure security (#97)
Browse files Browse the repository at this point in the history
  • Loading branch information
darcyYe authored Feb 15, 2023
1 parent 5d4b59d commit 43b8f58
Show file tree
Hide file tree
Showing 8 changed files with 23 additions and 381 deletions.
78 changes: 3 additions & 75 deletions packages/connector-oidc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ For detailed steps on registering a Google App for Logto's social sign-in use, p

## Compose the connector JSON

You can choose which OIDC authorization flow to use by configuring `oidcFlowType` as either 'AuthorizationCode', 'Implicit' or 'Hybrid'.

`scope` determines the resource you can access to after a successful authorization.

`clientId` and `clientSecret` can be found at your Google app details page.
Expand All @@ -32,6 +30,8 @@ Since an authentication request is required for all different flow types, an `au

Here are some examples of OIDC connector config JSON connected to Google. Other vendor's config could vary.

You may be curious as to why a standard OIDC protocol supports both the implicit and hybrid flows, yet the Logto connector only supports the authorization flow. It has been determined that the implicit and hybrid flows are less secure than the authorization flow. Due to Logto's focus on security, it only supports the authorization flow for the highest level of security for its users, despite its slightly less convenient nature.

### Authorization Code Flow

`responseType` can only be 'code' with authorization code flow, so we make it optional and a default value will be automatically filled.
Expand All @@ -40,7 +40,6 @@ Here are some examples of OIDC connector config JSON connected to Google. Other

```json
{
"oidcFlowType": "AuthorizationCode",
"scope": "profile email",
"responseType": "<OPTIONAL-'code'>",
"clientId": "<your-client-id>",
Expand Down Expand Up @@ -68,77 +67,6 @@ Here are some examples of OIDC connector config JSON connected to Google. Other
}
```

### Implicit Flow

`responseType` of implicit flow should either be 'id_token token' or 'id_token'.

`tokenEndpoint` in this flow is not required.

```json
{
"oidcFlowType": "Implicit",
"scope": "profile email",
"responseType": "<OPTIONAL-'id_token token'>",
"clientId": "<your-client-id>",
"clientSecret": "<your-client-secret>",
"authorizationEndpoint": "<vendor-authorization-endpoint>",
"idTokenVerificationConfig": {
"jwksUri": "<vendor's-jwks-uri>",
"issuer": "<vendor's-token-issuer>",
},
"authRequestOptionalConfig": {
"responseMode": "<OPTIONAL-response-mode>",
"display": "<OPTIONAL-display>",
"prompt": "<OPTIONAL-prompt>",
"maxAge": "<OPTIONAL-max-age>",
"uiLocales": "<OPTIONAL-ui-locales>",
"idTokenHint": "<OPTIONAL-id-token-hint>",
"loginHint": "<OPTIONAL-login-hint>",
"acrValues": "<OPTIONAL-acr-values>",
},,
"customConfig": {
"customParameter1": "<custom-parameter-1>",
"customParameter2": "<custom-parameter-2>"
}
}
```

### Hybrid Flow

`responseType` in hybrid flow can be one of 'id_token code', 'id_token code token', 'code token'.

If 'id_token' is not presented in `responseType`, then `tokenEndpoint` is required for fetching `idToken` via token request.

```json
{
"oidcFlowType": "Implicit",
"scope": "profile email",
"responseType": "<OPTIONAL-'code id_token token'>",
"clientId": "<your-client-id>",
"clientSecret": "<your-client-secret>",
"authorizationEndpoint": "<vendor-authorization-endpoint>",
"tokenEndpoint": "<OPTIONAL-vendor-token-endpoint>",
"idTokenVerificationConfig": {
"jwksUri": "<vendor's-jwks-uri>",
"issuer": "<vendor's-token-issuer>",
},
"authRequestOptionalConfig": {
"responseMode": "<OPTIONAL-response-mode>",
"display": "<OPTIONAL-display>",
"prompt": "<OPTIONAL-prompt>",
"maxAge": "<OPTIONAL-max-age>",
"uiLocales": "<OPTIONAL-ui-locales>",
"idTokenHint": "<OPTIONAL-id-token-hint>",
"loginHint": "<OPTIONAL-login-hint>",
"acrValues": "<OPTIONAL-acr-values>",
},,
"customConfig": {
"customParameter1": "<OPTIONAL-custom-parameter-1>",
"customParameter2": "<OPTIONAL-custom-parameter-2>"
}
}
```

> ℹ️ **Note**
>
> For all flow types, we provided an OPTIONAL `customConfig` key to put your customize parameters.
Expand All @@ -148,11 +76,11 @@ If 'id_token' is not presented in `responseType`, then `tokenEndpoint` is requir

| Name | Type | Required |
|-------------------------------------|-------------------------------------|-----------|
| oidcFlowType | enum | True |
| scope | string | True |
| clientId | string | True |
| clientSecret | string | True |
| authorizationEndpoint | string | True |
| tokenEndpoint | string | True |
| idTokenVerificationConfig | IdTokenVerificationConfig | True |
| authRequestOptionalConfig | AuthRequestOptionalConfig | False |
| customConfig | Record<string, string> | False |
Expand Down
1 change: 0 additions & 1 deletion packages/connector-oidc/docs/config-template.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"oidcFlowType": "AuthorizationCode",
"authorizationEndpoint": "<authorization-endpoint>",
"tokenEndpoint": "<token-endpoint>",
"clientId": "<client-id>",
Expand Down
1 change: 1 addition & 0 deletions packages/connector-oidc/package.extend.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "1.0.0-beta.18",
"description": "OIDC standard connector implementation.",
"dependencies": {
"@logto/core-kit": "1.0.0-beta.30",
"jose": "^4.3.8",
"nanoid": "^4.0.0"
}
Expand Down
36 changes: 12 additions & 24 deletions packages/connector-oidc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,18 @@ import {
validateConfig,
ConnectorType,
} from '@logto/connector-kit';
import { generateStandardId } from '@logto/core-kit';
import { assert, conditional, pick } from '@silverhand/essentials';
import { HTTPError } from 'got';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import snakecaseKeys from 'snakecase-keys';

import { defaultMetadata } from './constant.js';
import type { OidcConfig } from './types.js';
import { idTokenProfileStandardClaimsGuard, oidcConfigGuard, OidcFlowType } from './types.js';
import {
buildIdGenerator,
isIdTokenInResponseType,
getAuthorizationCodeFlowIdToken,
getImplicitFlowIdToken,
getHybridFlowIdToken,
} from './utils.js';
import { idTokenProfileStandardClaimsGuard, oidcConfigGuard } from './types.js';
import { getIdToken } from './utils.js';

const generateNonce = () => buildIdGenerator(12)();
const generateNonce = () => generateStandardId();

const getAuthorizationUri =
(getConfig: GetConnectorConfig): GetAuthorizationUri =>
Expand All @@ -37,7 +32,6 @@ const getAuthorizationUri =
const parsedConfig = oidcConfigGuard.parse(config);

const nonce = generateNonce();
const needNonce = isIdTokenInResponseType(parsedConfig.responseType);

assert(
setSession,
Expand All @@ -56,26 +50,13 @@ const getAuthorizationUri =
...authRequestOptionalConfig,
...customConfig,
}),
...(needNonce ? { nonce } : {}),
nonce,
redirect_uri: redirectUri,
});

return `${parsedConfig.authorizationEndpoint}?${queryParameters.toString()}`;
};

const getIdToken = async (config: OidcConfig, data: unknown, redirectUri?: string) => {
if (config.oidcFlowType === OidcFlowType.AuthorizationCode) {
return getAuthorizationCodeFlowIdToken(config, data, redirectUri);
}

if (config.oidcFlowType === OidcFlowType.Implicit) {
return getImplicitFlowIdToken(data);
}

// Hybrid Flow
return getHybridFlowIdToken(config, data, redirectUri);
};

const getUserInfo =
(getConfig: GetConnectorConfig): GetUserInfo =>
// eslint-disable-next-line complexity
Expand All @@ -92,6 +73,13 @@ const getUserInfo =
);
const { nonce: validationNonce, redirectUri } = await getSession();

assert(
redirectUri,
new ConnectorError(ConnectorErrorCodes.General, {
message: "CAN NOT find 'redirectUri' from connector session.",
})
);

const { id_token: idToken } = await getIdToken(parsedConfig, data, redirectUri);

if (!idToken) {
Expand Down
46 changes: 1 addition & 45 deletions packages/connector-oidc/src/types.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
scopePostProcessor,
implicitFlowResponsePostProcessor,
hybridFlowResponsePostProcessor,
} from './types.js';
import { scopePostProcessor } from './types.js';

describe('scopePostProcessor', () => {
it('`openid` will be added if not exists (with empty string)', () => {
Expand All @@ -17,43 +13,3 @@ describe('scopePostProcessor', () => {
expect(scopePostProcessor('profile openid')).toEqual('profile openid');
});
});

describe('implicitFlowResponsePostProcessor', () => {
it('return fully space-deliminated response type', () => {
expect(implicitFlowResponsePostProcessor('')).toEqual('id_token token');
});

it('throws when required `id_token` is not presented', () => {
expect(() => implicitFlowResponsePostProcessor('token')).toThrow();
});

it('throws when invalid type is not presented', () => {
expect(() => implicitFlowResponsePostProcessor('id_token hello')).toThrow();
});

it('return original response type', () => {
expect(implicitFlowResponsePostProcessor('id_token')).toEqual('id_token');
});
});

describe('hybridFlowResponsePostProcessor', () => {
it('return fully space-deliminated response type', () => {
expect(hybridFlowResponsePostProcessor('')).toEqual('id_token code token');
});

it('throws when required `code` is not presented', () => {
expect(() => hybridFlowResponsePostProcessor('id_token token')).toThrow();
});

it('throws when invalid type is not presented', () => {
expect(() => hybridFlowResponsePostProcessor('code hello')).toThrow();
});

it('throws when `token` or `id_token` is not presented', () => {
expect(() => hybridFlowResponsePostProcessor('code')).toThrow();
});

it('return original response type', () => {
expect(hybridFlowResponsePostProcessor('id_token code token')).toEqual('id_token code token');
});
});
Loading

0 comments on commit 43b8f58

Please sign in to comment.