Skip to content

Commit 94b7747

Browse files
committed
feat(pingidentity): add PingFederate auth provider
Signed-off-by: Jessica He <jhe@redhat.com>
1 parent 5adec44 commit 94b7747

18 files changed

Lines changed: 1942 additions & 97 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Auth Module: PingFederate Provider
2+
3+
This module provides a PingFederate auth provider implementation for `@backstage/plugin-auth-backend`.
4+
5+
The provider uses OIDC authentication and includes PingFederate-specific sign-in resolvers for matching users via LDAP UUID and Ping Identity user IDs.
6+
7+
> **Note:** This provider has only been tested with LDAP catalog provider (`@backstage/plugin-catalog-backend-module-ldap`). It has not been tested with the PingOne catalog provider (`@backstage-community/plugin-catalog-backend-module-pingidentity`), though it should work with appropriate resolver configuration.
8+
9+
## Installation
10+
11+
```bash
12+
yarn add --cwd packages/backend @backstage-community/plugin-auth-backend-module-pingfederate-provider
13+
```
14+
15+
## Configuration
16+
17+
> **Important:** You must configure at least one sign-in resolver. There is no default resolver - the `resolver` field is mandatory.
18+
19+
Add the following to your `app-config.yaml`:
20+
21+
```yaml
22+
auth:
23+
environment: development
24+
providers:
25+
pingfederate:
26+
development:
27+
# Base URL of your PingFederate server (without the .well-known path)
28+
baseUrl: ${PINGFEDERATE_BASE_URL} # e.g., https://your-pingfederate-server.com
29+
clientId: ${PINGFEDERATE_CLIENT_ID}
30+
clientSecret: ${PINGFEDERATE_CLIENT_SECRET}
31+
signIn:
32+
resolvers:
33+
# Resolver is required - choose one that matches your catalog setup:
34+
35+
# Option 1: Match LDAP UUID annotation
36+
- resolver: ldapUuidMatchingAnnotation
37+
ldapUuidKey: 'ldap_uuid' # optional, defaults to 'ldap_uuid'
38+
39+
40+
# Option 2: Match Ping Identity user ID annotation
41+
# - resolver: subClaimMatchingPingIdentityUserId
42+
43+
# Option 3: Match email local part to user entity name
44+
# - resolver: emailLocalPartMatchingUserEntityName
45+
```
46+
47+
### Optional Configuration
48+
49+
You can customize the provider behavior with additional configuration options:
50+
51+
```yaml
52+
auth:
53+
providers:
54+
pingfederate:
55+
development:
56+
baseUrl: ${PINGFEDERATE_BASE_URL}
57+
clientId: ${PINGFEDERATE_CLIENT_ID}
58+
clientSecret: ${PINGFEDERATE_CLIENT_SECRET}
59+
60+
# Optional: Request additional scopes beyond the default openid, profile, email
61+
additionalScopes:
62+
- offline_access
63+
# Or as a single string:
64+
# additionalScopes: offline_access
65+
66+
# Optional: Control the authentication prompt behavior
67+
# 'none' (default) - SSO if session exists, 'login' - force login, 'auto' - let provider decide
68+
prompt: none
69+
70+
# Optional: Override the callback URL if different from the default
71+
# callbackUrl: https://backstage.example.com/api/auth/pingfederate/handler/frame
72+
73+
signIn:
74+
resolvers:
75+
- resolver: ldapUuidMatchingAnnotation
76+
```
77+
78+
## Register the Provider
79+
80+
Add the following to your `packages/backend/src/index.ts`:
81+
82+
```typescript
83+
const backend = createBackend();
84+
backend.add(
85+
import(
86+
'@backstage-community/plugin-auth-backend-module-pingfederate-provider'
87+
),
88+
);
89+
```
90+
91+
## Available Sign-In Resolvers
92+
93+
You must configure at least one resolver in your `app-config.yaml`. Choose the resolver that matches your catalog setup and identity provider configuration.
94+
95+
### PingFederate-Specific Resolvers
96+
97+
- **`ldapUuidMatchingAnnotation`** - Matches an LDAP UUID claim to the `backstage.io/ldap-uuid` annotation
98+
99+
- Configuration options:
100+
- `ldapUuidKey` (optional, default: `'ldap_uuid'`) - The claim name containing the LDAP UUID
101+
- `dangerouslyAllowSignInWithoutUserInCatalog` (optional) - Allow sign-in even if user is not in catalog
102+
- Validates that the UUID in userinfo matches the UUID in the ID token
103+
- **Requires PingFederate LDAP UUID configuration** (see setup instructions below)
104+
105+
- **`subClaimMatchingPingIdentityUserId`** - Matches the `sub` claim to the `pingidentity.org/id` annotation
106+
- Configuration options:
107+
- `dangerouslyAllowSignInWithoutUserInCatalog` (optional) - Allow sign-in even if user is not in catalog
108+
- Best for PingOne catalog provider integration
109+
- Validates that the `sub` claim in userinfo matches the `sub` in the ID token
110+
111+
#### PingFederate Configuration for LDAP UUID Resolver
112+
113+
The `ldapUuidMatchingAnnotation` resolver requires PingFederate to be configured to expose the LDAP UUID attribute. Follow these steps:
114+
115+
**1. Configure Authentication Policy Contract**
116+
117+
1. Create a contract named `rhdh-contract`
118+
2. Add Attribute Source: Link your LDAP Data Store to this contract
119+
3. Set Search Filter: Use `sAMAccountName=${username}` (or appropriate filter for your LDAP)
120+
4. Expose the LDAP UUID attribute under the Authentication Policy Contract Mapping
121+
5. For Active Directory, add the UUID field called `objectGUID` (note: UUID attribute name varies by LDAP vendor)
122+
6. Set Encoding type to `Hex` from the dropdown
123+
7. Enter search filter as needed (e.g., `sAMAccountName=${username}`)
124+
125+
**2. Map UUID to Sub Claim**
126+
127+
Under Contract Fulfillment, map `sub` to the objectGUID from LDAP:
128+
129+
1. From Source dropdown, select `Expression`
130+
2. Enter the following OGNL expression to format the LDAP UUID as expected by Backstage:
131+
132+
```java
133+
#GUID = #this.get("ds.<ldap-data-source-id>.objectGUID").toString(),
134+
#GUID.substring(6,8) + #GUID.substring(4,6) + #GUID.substring(2,4) + #GUID.substring(0,2) + "-" +
135+
#GUID.substring(10,12) + #GUID.substring(8,10) + "-" +
136+
#GUID.substring(14,16) + #GUID.substring(12,14) + "-" +
137+
#GUID.substring(16,20) + "-" + #GUID.substring(20,32)
138+
```
139+
140+
Replace `<ldap-data-source-id>` with your actual LDAP data source ID.
141+
142+
**3. Configure OAuth & OIDC Scopes**
143+
144+
1. Navigate to **System > OAuth Scopes**
145+
2. Ensure `email` and `profile` are added as Common Scopes
146+
147+
**4. Bridge Contract to OIDC Policy**
148+
149+
Use the Access Token as a bridge to ensure the `sub` claim is delivered consistently:
150+
151+
1. **Access Token Mapping**: Create a mapping from `rhdh-contract` to your Access Token Manager. Map the `sub` field from the contract to the token.
152+
2. **OIDC Policy Fulfillment**: In your OIDC Policy, fulfill the `sub` claim by selecting Access Token as the source and `sub` as the value.
153+
3. **Enable Delivery**: In the OIDC Policy Attribute Contract, ensure the ID Token and UserInfo checkboxes are selected for the `sub` claim.
154+
155+
**5. (Optional) Expose UUID under `ldap_uuid` claim in UserInfo**
156+
157+
If you want the UUID available under a custom `ldap_uuid` claim in UserInfo (instead of the default `sub` claim):
158+
159+
1. Under **OIDC Policy Attribute Contract**:
160+
- Extend the contract with `ldap_uuid` under Attribute Contract
161+
- Under Contract Fulfillment, map `ldap_uuid` to the UUID value via Access Token
162+
163+
Then configure your Backstage auth to use:
164+
165+
```yaml
166+
signIn:
167+
resolvers:
168+
- resolver: ldapUuidMatchingAnnotation
169+
ldapUuidKey: 'ldap_uuid' # Custom claim name
170+
```
171+
172+
### Common Resolvers
173+
174+
- **`emailLocalPartMatchingUserEntityName`** - Matches the local part of the email to the user entity name
175+
- Configuration options:
176+
- `allowedDomains` (optional) - Restrict sign-in to specific email domains
177+
- `dangerouslyAllowSignInWithoutUserInCatalog` (optional) - Allow sign-in even if user is not in catalog
178+
- **`emailMatchingUserEntityProfileEmail`** - Matches the email to the user entity's email
179+
- Configuration options:
180+
- `dangerouslyAllowSignInWithoutUserInCatalog` (optional) - Allow sign-in even if user is not in catalog
181+
182+
## Links
183+
184+
- [Repository](https://github.com/backstage/community-plugins/tree/main/workspaces/pingidentity/plugins/auth-backend-module-pingfederate-provider)
185+
- [Backstage Project Homepage](https://backstage.io)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: backstage.io/v1alpha1
2+
kind: Component
3+
metadata:
4+
name: backstage-community-plugin-auth-backend-module-pingfederate-provider
5+
title: '@backstage-community/plugin-auth-backend-module-pingfederate-provider'
6+
description: The PingFederate provider backend module for the auth plugin.
7+
spec:
8+
lifecycle: experimental
9+
type: backstage-backend-plugin-module
10+
owner: maintainers
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2026 The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import { HumanDuration } from '@backstage/types';
17+
18+
export interface Config {
19+
auth?: {
20+
providers?: {
21+
/** @visibility frontend */
22+
pingfederate?: {
23+
[authEnv: string]: {
24+
clientId: string;
25+
/**
26+
* @visibility secret
27+
*/
28+
clientSecret: string;
29+
baseUrl: string;
30+
callbackUrl?: string;
31+
additionalScopes?: string | string[];
32+
prompt?: string;
33+
signIn?: {
34+
resolvers: Array<
35+
| {
36+
resolver: 'emailLocalPartMatchingUserEntityName';
37+
allowedDomains?: string[];
38+
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
39+
}
40+
| {
41+
resolver: 'emailMatchingUserEntityProfileEmail';
42+
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
43+
}
44+
| {
45+
resolver: 'ldapUuidMatchingAnnotation';
46+
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
47+
ldapUuidKey?: string;
48+
}
49+
| {
50+
resolver: 'subClaimMatchingPingIdentityUserId';
51+
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
52+
}
53+
>;
54+
};
55+
sessionDuration?: HumanDuration | string;
56+
};
57+
};
58+
};
59+
};
60+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright 2023 The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { createBackend } from '@backstage/backend-defaults';
18+
19+
const backend = createBackend();
20+
21+
backend.add(import('@backstage/plugin-auth-backend'));
22+
backend.add(import('../src'));
23+
24+
backend.start();
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Knip report
2+
3+
## Unused dependencies (1)
4+
5+
| Name | Location | Severity |
6+
| :--------------- | :---------------- | :------- |
7+
| @backstage/types | package.json:36:6 | error |
8+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "@backstage-community/plugin-auth-backend-module-pingfederate-provider",
3+
"description": "The PingFederate provider backend module for the auth plugin.",
4+
"version": "0.0.0",
5+
"main": "src/index.ts",
6+
"types": "src/index.ts",
7+
"license": "Apache-2.0",
8+
"publishConfig": {
9+
"access": "public",
10+
"main": "dist/index.cjs.js",
11+
"types": "dist/index.d.ts"
12+
},
13+
"repository": {
14+
"type": "git",
15+
"url": "https://github.com/backstage/community-plugins",
16+
"directory": "workspaces/pingidentity/plugins/auth-backend-module-pingfederate-provider"
17+
},
18+
"backstage": {
19+
"role": "backend-plugin-module",
20+
"pluginId": "auth",
21+
"pluginPackage": "@backstage/plugin-auth-backend"
22+
},
23+
"scripts": {
24+
"start": "backstage-cli package start",
25+
"build": "backstage-cli package build",
26+
"lint": "backstage-cli package lint",
27+
"test": "backstage-cli package test",
28+
"clean": "backstage-cli package clean",
29+
"prepack": "backstage-cli package prepack",
30+
"tsc": "tsc"
31+
},
32+
"dependencies": {
33+
"@backstage/backend-plugin-api": "^1.8.0",
34+
"@backstage/config": "^1.3.6",
35+
"@backstage/plugin-auth-node": "^0.7.0",
36+
"@backstage/types": "^1.2.2",
37+
"express": "^4.22.0",
38+
"jose": "^5.0.0",
39+
"openid-client": "^5.5.0",
40+
"zod": "^3.25.76 || ^4.0.0"
41+
},
42+
"devDependencies": {
43+
"@backstage/backend-defaults": "^0.16.0",
44+
"@backstage/backend-test-utils": "^1.11.1",
45+
"@backstage/cli": "^0.36.0",
46+
"@backstage/plugin-auth-backend": "^0.27.3",
47+
"@types/supertest": "^6.0.0",
48+
"msw": "^1.3.1",
49+
"supertest": "^7.0.0"
50+
},
51+
"files": [
52+
"dist",
53+
"config.d.ts"
54+
],
55+
"configSchema": "config.d.ts"
56+
}

0 commit comments

Comments
 (0)