Skip to content

Commit d37a997

Browse files
authored
Add oidcLdapUuidMatchingAnnotation sign in resolver (#2937)
Signed-off-by: Jessica He <jhe@redhat.com>
1 parent a11d819 commit d37a997

4 files changed

Lines changed: 219 additions & 159 deletions

File tree

packages/backend/src/modules/authProvidersModule.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ import {
7070
} from '@backstage/plugin-auth-node';
7171

7272
import { TransitiveGroupOwnershipResolver } from '../transitiveGroupOwnershipResolver';
73-
import { rhdhSignInResolvers } from './authResolvers';
73+
import { trySignInResolvers } from './resolverUtils';
74+
import { rhdhSignInResolvers } from './rhdhSignInResolvers';
7475

7576
function getAuthProviderFactory(providerId: string): AuthProviderFactory {
7677
switch (providerId) {
@@ -183,12 +184,17 @@ function getAuthProviderFactory(providerId: string): AuthProviderFactory {
183184
case 'oidc':
184185
return createOAuthProviderFactory({
185186
authenticator: oidcAuthenticator,
186-
signInResolver: rhdhSignInResolvers.oidcSubClaimMatchingIdPUserId(),
187+
signInResolver: trySignInResolvers([
188+
rhdhSignInResolvers.oidcSubClaimMatchingKeycloakUserId(),
189+
rhdhSignInResolvers.oidcLdapUuidMatchingAnnotation(),
190+
]),
187191
signInResolverFactories: {
188192
oidcSubClaimMatchingKeycloakUserId:
189193
rhdhSignInResolvers.oidcSubClaimMatchingKeycloakUserId,
190194
oidcSubClaimMatchingPingIdentityUserId:
191195
rhdhSignInResolvers.oidcSubClaimMatchingPingIdentityUserId,
196+
oidcLdapUuidMatchingAnnotation:
197+
rhdhSignInResolvers.oidcLdapUuidMatchingAnnotation,
192198
...oidcSignInResolvers,
193199
...commonSignInResolvers,
194200
},

packages/backend/src/modules/authResolvers.ts

Lines changed: 0 additions & 157 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import type { OidcAuthResult } from '@backstage/plugin-auth-backend-module-oidc-provider';
2+
import {
3+
AuthResolverContext,
4+
createSignInResolverFactory,
5+
OAuthAuthenticatorResult,
6+
SignInInfo,
7+
SignInResolver,
8+
} from '@backstage/plugin-auth-node';
9+
10+
import { decodeJwt } from 'jose';
11+
import { z } from 'zod';
12+
13+
export type OidcProviderInfo = {
14+
userIdKey: string;
15+
providerName: string;
16+
};
17+
18+
/**
19+
* Creates an OIDC sign-in resolver that looks up the user using a specific annotation key.
20+
*
21+
* @param userIdKey - The annotation key to match the user's `sub` claim.
22+
* @param providerName - The name of the identity provider to report in error message if the `sub` claim is missing.
23+
*/
24+
export const createOidcSubClaimResolver = (provider: OidcProviderInfo) =>
25+
createSignInResolverFactory({
26+
optionsSchema: z
27+
.object({
28+
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
29+
})
30+
.optional(),
31+
create(options) {
32+
return async (
33+
info: SignInInfo<OAuthAuthenticatorResult<OidcAuthResult>>,
34+
ctx: AuthResolverContext,
35+
) => {
36+
const sub = info.result.fullProfile.userinfo.sub;
37+
if (!sub) {
38+
throw new Error(
39+
`The user profile from ${provider.providerName} is missing a 'sub' claim, likely due to a misconfiguration in the provider. Please contact your system administrator for assistance.`,
40+
);
41+
}
42+
43+
const idToken = info.result.fullProfile.tokenset.id_token;
44+
if (!idToken) {
45+
throw new Error(
46+
`The user ID token from ${provider.providerName} is missing. Please contact your system administrator for assistance.`,
47+
);
48+
}
49+
50+
const subFromIdToken = decodeJwt(idToken)?.sub;
51+
if (sub !== subFromIdToken) {
52+
throw new Error(
53+
`There was a problem verifying your identity with ${provider.providerName} due to a mismatching 'sub' claim. Please contact your system administrator for assistance.`,
54+
);
55+
}
56+
57+
return await ctx.signInWithCatalogUser(
58+
{
59+
annotations: { [provider.userIdKey]: sub },
60+
},
61+
sub,
62+
options?.dangerouslyAllowSignInWithoutUserInCatalog,
63+
);
64+
};
65+
},
66+
});
67+
68+
/**
69+
* Creates a sign in resolver that tries the provided list of sign in resolvers
70+
*
71+
* @param signInResolvers list of sign in resolvers to try
72+
*/
73+
export function trySignInResolvers<TAuthResult>(
74+
signInResolvers: SignInResolver<TAuthResult>[],
75+
): SignInResolver<TAuthResult> {
76+
return async (profile, context) => {
77+
for (const resolver of Object.values(signInResolvers)) {
78+
try {
79+
return await resolver(profile, context);
80+
} catch (error) {
81+
continue;
82+
}
83+
}
84+
85+
// same error message as in upstream readDeclarativeSignInResolver
86+
throw new Error(
87+
'Failed to sign-in, unable to resolve user identity. Please verify that your catalog contains the expected User entities that would match your configured sign-in resolver.',
88+
);
89+
};
90+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { OAuth2ProxyResult } from '@backstage/plugin-auth-backend-module-oauth2-proxy-provider';
2+
import type { OidcAuthResult } from '@backstage/plugin-auth-backend-module-oidc-provider';
3+
import {
4+
AuthResolverContext,
5+
createSignInResolverFactory,
6+
OAuthAuthenticatorResult,
7+
SignInInfo,
8+
} from '@backstage/plugin-auth-node';
9+
10+
import { decodeJwt } from 'jose';
11+
import { z } from 'zod';
12+
13+
import { createOidcSubClaimResolver, OidcProviderInfo } from './resolverUtils';
14+
15+
const KEYCLOAK_INFO: OidcProviderInfo = {
16+
userIdKey: 'keycloak.org/id',
17+
providerName: 'Keycloak',
18+
};
19+
20+
const PING_IDENTITY_INFO: OidcProviderInfo = {
21+
userIdKey: 'pingidentity.org/id',
22+
providerName: 'Ping Identity',
23+
};
24+
25+
const LDAP_UUID_ANNOTATION = 'backstage.io/ldap-uuid';
26+
27+
/**
28+
* Additional RHDH specific sign-in resolvers.
29+
*
30+
* @public
31+
*/
32+
export namespace rhdhSignInResolvers {
33+
/**
34+
* An OIDC resolver that looks up the user using their Keycloak user ID.
35+
*/
36+
export const oidcSubClaimMatchingKeycloakUserId =
37+
createOidcSubClaimResolver(KEYCLOAK_INFO);
38+
39+
/**
40+
* An OIDC resolver that looks up the user using their Ping Identity user ID.
41+
*/
42+
export const oidcSubClaimMatchingPingIdentityUserId =
43+
createOidcSubClaimResolver(PING_IDENTITY_INFO);
44+
45+
/**
46+
* An oauth2proxy resolver that looks up the user using the OAUTH_USER_HEADER environment variable,
47+
* 'x-forwarded-preferred-username' or 'x-forwarded-user'.
48+
*/
49+
export const oauth2ProxyUserHeaderMatchingUserEntityName =
50+
createSignInResolverFactory({
51+
optionsSchema: z
52+
.object({
53+
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
54+
})
55+
.optional(),
56+
create(options) {
57+
return async (
58+
info: SignInInfo<OAuth2ProxyResult>,
59+
ctx: AuthResolverContext,
60+
) => {
61+
const name = process.env.OAUTH_USER_HEADER
62+
? info.result.getHeader(process.env.OAUTH_USER_HEADER)
63+
: info.result.getHeader('x-forwarded-preferred-username') ||
64+
info.result.getHeader('x-forwarded-user');
65+
if (!name) {
66+
throw new Error('Request did not contain a user');
67+
}
68+
return ctx.signInWithCatalogUser(
69+
{
70+
entityRef: { name },
71+
},
72+
name,
73+
options?.dangerouslyAllowSignInWithoutUserInCatalog,
74+
);
75+
};
76+
},
77+
});
78+
79+
export const oidcLdapUuidMatchingAnnotation = createSignInResolverFactory({
80+
optionsSchema: z
81+
.object({
82+
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
83+
})
84+
.optional(),
85+
create(options) {
86+
return async (
87+
info: SignInInfo<OAuthAuthenticatorResult<OidcAuthResult>>,
88+
ctx: AuthResolverContext,
89+
) => {
90+
const uuid = info.result.fullProfile.userinfo.ldap_uuid as string;
91+
if (!uuid) {
92+
throw new Error(
93+
`The user profile from LDAP is missing the UUID, likely due to a misconfiguration in the provider. Please contact your system administrator for assistance.`,
94+
);
95+
}
96+
97+
const idToken = info.result.fullProfile.tokenset.id_token;
98+
if (!idToken) {
99+
throw new Error(
100+
`The user ID token from LDAP is missing. Please contact your system administrator for assistance.`,
101+
);
102+
}
103+
104+
const uuidFromIdToken = decodeJwt(idToken)?.ldap_uuid;
105+
if (uuid !== uuidFromIdToken) {
106+
throw new Error(
107+
`There was a problem verifying your identity with LDAP due to mismatching UUID. Please contact your system administrator for assistance.`,
108+
);
109+
}
110+
111+
return ctx.signInWithCatalogUser(
112+
{
113+
annotations: { [LDAP_UUID_ANNOTATION]: uuid },
114+
},
115+
uuid,
116+
options?.dangerouslyAllowSignInWithoutUserInCatalog,
117+
);
118+
};
119+
},
120+
});
121+
}

0 commit comments

Comments
 (0)