Add ability to map oidc groups to Directus roles (#24157)

* add oidc group mapping

* add changeset and contributors.yml

* add back upstream changes

* Run formatter

* Fix capitalization in docs description

* Re-add lost docs after merge conflict resolution

* Make groups claim name configurable

* Add OIDC to dictionary

* Improve groups check

Co-authored-by: Aiden Foxx <aiden.foxx.mail@gmail.com>

* Add log message if OIDC group claim is empty

* Make groups claim optional

---------

Co-authored-by: Rijk van Zanten <rijkvanzanten@me.com>
Co-authored-by: Aiden Foxx <aiden.foxx.mail@gmail.com>
This commit is contained in:
Johannes
2024-12-10 21:48:35 +00:00
committed by GitHub
parent 5b59bbb41f
commit feeda04ad8
6 changed files with 84 additions and 21 deletions

View File

@@ -0,0 +1,6 @@
---
'@directus/api': minor
'docs': minor
---
Added ability to map oidc groups to oidc roles

View File

@@ -26,6 +26,7 @@ import { createDefaultAccountability } from '../../permissions/utils/create-defa
import { AuthenticationService } from '../../services/authentication.js';
import { UsersService } from '../../services/users.js';
import type { AuthData, AuthDriverOptions, User } from '../../types/index.js';
import type { RoleMap } from '../../types/rolemap.js';
import asyncHandler from '../../utils/async-handler.js';
import { getConfigFromEnv } from '../../utils/get-config-from-env.js';
import { getIPFromReq } from '../../utils/get-ip-from-req.js';
@@ -39,6 +40,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
redirectUrl: string;
usersService: UsersService;
config: Record<string, any>;
roleMap: RoleMap;
constructor(options: AuthDriverOptions, config: Record<string, any>) {
super(options, config);
@@ -69,6 +71,23 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
this.redirectUrl = redirectUrl.toString();
this.usersService = new UsersService({ knex: this.knex, schema: this.schema });
this.config = additionalConfig;
this.roleMap = {};
const roleMapping = this.config['roleMapping'];
if (roleMapping) {
this.roleMap = roleMapping;
}
// role mapping will fail on login if AUTH_<provider>_ROLE_MAPPING is an array instead of an object.
// This happens if the 'json:' prefix is missing from the variable declaration. To save the user from exhaustive debugging, we'll try to fail early here.
if (roleMapping instanceof Array) {
logger.error(
"[OpenID] Expected a JSON-Object as role mapping, got an Array instead. Make sure you declare the variable with 'json:' prefix.",
);
throw new InvalidProviderError();
}
this.client = new Promise((resolve, reject) => {
Issuer.discover(issuerUrl)
@@ -178,6 +197,22 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
throw handleError(e);
}
let role = this.config['defaultRoleId'];
const groupClaimName: string = this.config['groupClaimName'] ?? 'groups';
const groups = userInfo[groupClaimName];
if (Array.isArray(groups)) {
for (const key in this.roleMap) {
if (groups.includes(key)) {
// Overwrite default role if user is member of a group specified in roleMap
role = this.roleMap[key];
break;
}
}
} else {
logger.debug(`[OpenID] Configured group claim with name "${groupClaimName}" does not exist or is empty.`);
}
// Flatten response to support dot indexes
userInfo = flatten(userInfo) as Record<string, unknown>;
@@ -198,7 +233,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
last_name: userInfo['family_name'],
email: email,
external_identifier: identifier,
role: this.config['defaultRoleId'],
role: role,
auth_data: tokenSet.refresh_token && JSON.stringify({ refreshToken: tokenSet.refresh_token }),
};
@@ -209,6 +244,8 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
// user that is about to be updated
let emitPayload: Record<string, unknown> = {
auth_data: userPayload.auth_data,
// Make sure a user's role gets updated if his openid group or role mapping changes
role: role,
};
if (syncUserInfo) {

3
api/src/types/rolemap.ts Normal file
View File

@@ -0,0 +1,3 @@
export type RoleMap = {
[key: string]: string;
};

View File

@@ -188,5 +188,6 @@
- Zyles
- eremannisto
- m3Lith
- Audiotape-2
- vst-name
- clonefetch

View File

@@ -68,6 +68,7 @@
(S|s)ubheader
(S|s)upabase
(T|t)extarea
(T|t)heming
(T|t)imeseries
(T|t)ooltips?
(T|t)riage
@@ -173,10 +174,10 @@ customizable
customizations
DateTime
DDoS
destructure
destructured
devs
Devtools
destructure
DigitalOcean
DigitalOcean's
directus
@@ -201,8 +202,8 @@ entrypoint
Entrypoint
env
ENV
ESM
esm
ESM
Exif
fallbacks
falsy
@@ -309,10 +310,11 @@ O2M
O2Ms
O2O
OAuth2?
OpenAI
OIDC
Okta
omnichannel
on-prem
OpenAI
OpenAPI
OpenID
OracleDB
@@ -414,7 +416,6 @@ SVGs?
TablePlus
tfa
TFA
(T|t)heming
TileJSON
TinyMCE
TLS

View File

@@ -859,22 +859,23 @@ Directus users "External Identifier".
OpenID is an authentication protocol built on OAuth 2.0, and should be preferred over standard OAuth 2.0 where possible.
| Variable | Description | Default Value |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------- |
| `AUTH_<PROVIDER>_CLIENT_ID` | Client identifier for the external service. | -- |
| `AUTH_<PROVIDER>_CLIENT_SECRET` | Client secret for the external service. | -- |
| `AUTH_<PROVIDER>_SCOPE` | A white-space separated list of permissions to request. | `openid profile email` |
| `AUTH_<PROVIDER>_ISSUER_URL` | OpenID `.well-known` discovery document URL of the external service. | -- |
| `AUTH_<PROVIDER>_IDENTIFIER_KEY` | User profile identifier key <sup>[1]</sup>. | `sub`<sup>[2]</sup> |
| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Automatically create accounts for authenticating users. | `false` |
| `AUTH_<PROVIDER>_REQUIRE_VERIFIED_EMAIL` | Require created users to have a verified email address. | `false` |
| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID` | A Directus role ID to assign created users. | -- |
| `AUTH_<PROVIDER>_SYNC_USER_INFO` | Set user's first name, last name and email from provider's user info on each login. | `false` |
| `AUTH_<PROVIDER>_ICON` | SVG icon to display with the login link. [See options here](/user-guide/overview/glossary#icons). | `account_circle` |
| `AUTH_<PROVIDER>_LABEL` | Text to be presented on SSO button within App. | `<PROVIDER>` |
| `AUTH_<PROVIDER>_PARAMS` | Custom query parameters applied to the authorization URL. | -- |
| `AUTH_<PROVIDER>_REDIRECT_ALLOW_LIST` | A comma-separated list of external URLs (including paths) allowed for redirecting after successful login. | -- |
| `AUTH_<PROVIDER>_LOGIN_TIMEOUT` | Time-Out for successful login in SSO. | `5m` |
| Variable | Description | Default Value |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |
| `AUTH_<PROVIDER>_CLIENT_ID` | Client identifier for the external service. | -- |
| `AUTH_<PROVIDER>_CLIENT_SECRET` | Client secret for the external service. | -- |
| `AUTH_<PROVIDER>_SCOPE` | A white-space separated list of permissions to request. | `openid profile email` |
| `AUTH_<PROVIDER>_ISSUER_URL` | OpenID `.well-known` discovery document URL of the external service. | -- |
| `AUTH_<PROVIDER>_IDENTIFIER_KEY` | User profile identifier key <sup>[1]</sup>. | `sub`<sup>[2]</sup> |
| `AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION` | Automatically create accounts for authenticating users. | `false` |
| `AUTH_<PROVIDER>_REQUIRE_VERIFIED_EMAIL` | Require created users to have a verified email address. | `false` |
| `AUTH_<PROVIDER>_DEFAULT_ROLE_ID` | A Directus role ID to assign created users. | -- |
| `AUTH_<PROVIDER>_SYNC_USER_INFO` | Set user's first name, last name and email from provider's user info on each login. | `false` |
| `AUTH_<PROVIDER>_ICON` | SVG icon to display with the login link. [See options here](/user-guide/overview/glossary#icons). | `account_circle` |
| `AUTH_<PROVIDER>_LABEL` | Text to be presented on SSO button within App. | `<PROVIDER>` |
| `AUTH_<PROVIDER>_PARAMS` | Custom query parameters applied to the authorization URL. | -- |
| `AUTH_<PROVIDER>_REDIRECT_ALLOW_LIST` | A comma-separated list of external URLs (including paths) allowed for redirecting after successful login. | -- |
| `AUTH_<PROVIDER>_ROLE_MAPPING` | A JSON object in the form of `{ "openid_group_name": "directus_role_id" }` that you can use to map OpenID groups to Directus roles <sup>[3]</sup>. If not specified, falls back to `AUTH_<PROVIDER>_DEFAULT_ROLE_ID` | -- |
| `AUTH_<PROVIDER>_GROUP_CLAIM_NAME` | The name of the OIDC claim that contains your user's groups. | `groups` |
<sup>[1]</sup> When authenticating, Directus will match the identifier value from the external user profile to a
Directus users "External Identifier".
@@ -882,6 +883,20 @@ Directus users "External Identifier".
<sup>[2]</sup> `sub` represents a unique user identifier defined by the OpenID provider. For users not relying on
`PUBLIC_REGISTRATION` it is recommended to use a human-readable identifier, such as `email`.
<sup>[3]</sup> As directus only allows one role per user, evaluating stops after the first match. An OpenID user that is
member of both e.g. developer and admin groups may be assigned different roles depending on the order that you specify
your role-mapping in: In the following example said OpenID user will be assigned the role `directus_developer_role_id`
```
AUTH_<PROVIDER>_ROLE_MAPPING: json:{ "developer": "directus_developer_role_id", "admin": "directus_admin_role_id" }"
```
Whereas in the following example the OpenID user will be assigned the role `directus_admin_role_id`
```
AUTH_<PROVIDER>_ROLE_MAPPING: json:{ "admin": "directus_admin_role_id", "developer": "directus_developer_role_id" }"
```
### LDAP (`ldap`)
LDAP allows Active Directory users to authenticate and use Directus without having to be manually configured. User