Merge pull request #28967 from JessicaJHee/dangerouslyAllowSignInWithoutUserInCatalog-config

introduce dangerouslyAllowSignInWithoutUserInCatalog auth resolver config
This commit is contained in:
Patrik Oldsberg
2025-05-13 12:18:55 +02:00
committed by GitHub
75 changed files with 840 additions and 166 deletions
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Added support for the new `dangerousEntityRefFallback` option for `signInWithCatalogUser` in `AuthResolverContext`.
+38
View File
@@ -0,0 +1,38 @@
---
'@backstage/plugin-auth-node': patch
---
Added a new `dangerousEntityRefFallback` option to the `signInWithCatalogUser` method in `AuthResolverContext`. The option will cause the provided entity reference to be used as a fallback in case the user is not found in the catalog. It is up to the caller to provide the fallback entity reference.
Auth providers that include pre-defined sign-in resolvers are encouraged to define a flag named `dangerouslyAllowSignInWithoutUserInCatalog` in their config, which in turn enables use of the `dangerousEntityRefFallback` option. For example:
```ts
export const usernameMatchingUserEntityName = createSignInResolverFactory({
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
) => {
const { username } = info.result.fullProfile;
if (!username) {
throw new Error('User profile does not contain a username');
}
return ctx.signInWithCatalogUser(
{ entityRef: { name: username } },
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: username } }
: undefined,
},
);
};
},
});
```
+21
View File
@@ -0,0 +1,21 @@
---
'@backstage/plugin-auth-backend-module-cloudflare-access-provider': patch
'@backstage/plugin-auth-backend-module-bitbucket-server-provider': patch
'@backstage/plugin-auth-backend-module-azure-easyauth-provider': patch
'@backstage/plugin-auth-backend-module-oauth2-proxy-provider': patch
'@backstage/plugin-auth-backend-module-vmware-cloud-provider': patch
'@backstage/plugin-auth-backend-module-atlassian-provider': patch
'@backstage/plugin-auth-backend-module-bitbucket-provider': patch
'@backstage/plugin-auth-backend-module-microsoft-provider': patch
'@backstage/plugin-auth-backend-module-onelogin-provider': patch
'@backstage/plugin-auth-backend-module-aws-alb-provider': patch
'@backstage/plugin-auth-backend-module-gcp-iap-provider': patch
'@backstage/plugin-auth-backend-module-github-provider': patch
'@backstage/plugin-auth-backend-module-gitlab-provider': patch
'@backstage/plugin-auth-backend-module-google-provider': patch
'@backstage/plugin-auth-backend-module-oauth2-provider': patch
'@backstage/plugin-auth-backend-module-oidc-provider': patch
'@backstage/plugin-auth-backend-module-okta-provider': patch
---
Introduce `dangerouslyAllowSignInWithoutUserInCatalog` auth resolver config.
@@ -304,6 +304,7 @@ oidc
Okta
Olausson
Oldsberg
onboarded
onboarding
Onboarding
onelogin
+36 -2
View File
@@ -351,8 +351,12 @@ users to sign in, for example by checking email domains.
While populating the catalog with organizational data unlocks more powerful ways
to browse your software ecosystem, it might not always be a viable or prioritized
option. However, even if you do not have user entities populated in your catalog, you
can still sign in users. As there are currently no built-in sign-in resolvers for
this scenario you will need to implement your own.
can still sign in users.
##### Custom Sign-in Resolver to bypass user in catalog requirement
As there are currently no built-in sign-in resolvers for
this scenario you may want to implement your own.
Signing in a user that doesn't exist in the catalog is as simple as skipping the
catalog lookup step from the above example. Rather than looking up the user, we
@@ -446,6 +450,36 @@ return ctx.issueToken({
});
```
##### Using the `dangerouslyAllowSignInWithoutUserInCatalog` Option
Another way to bypass this requirement is to enable the `dangerouslyAllowSignInWithoutUserInCatalog` option for resolvers.
Users will still be authenticated as usual but this config will bypass the check that ensures the user is present in the catalog.
If the user entity is not found in the catalog, a Backstage user token will still be issued based on the identifying information available at the resolver level.
For example:
```yaml title="Within the provider configuration"
auth:
providers:
github:
development:
...
signIn:
resolvers:
- resolver: emailLocalPartMatchingUserEntityName
dangerouslyAllowSignInWithoutUserInCatalog: true
```
:::warning
Enabling this option in production poses security risks.
:::
This option may grant access to unexpected users who havent been onboarded into
Backstage. Since there is no user entity to associate with the signed-in user, permissions
may not apply as expected and they will have the same permissions as a guest user.
Careful consideration should be given to the permissions assigned to such users,
particularly when using the permission system.
## Profile Transforms
Similar to a custom sign-in resolver, you can also write a custom profile transform
+9 -2
View File
@@ -32,12 +32,19 @@ export interface Config {
additionalScopes?: string | string[];
signIn?: {
resolvers: Array<
| { resolver: 'usernameMatchingUserEntityName' }
| {
resolver: 'usernameMatchingUserEntityName';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -38,7 +38,8 @@
"@backstage/plugin-auth-node": "workspace:^",
"express": "^4.18.2",
"passport": "^0.7.0",
"passport-atlassian-oauth2": "^2.1.0"
"passport-atlassian-oauth2": "^2.1.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -20,7 +20,10 @@ export const atlassianAuthenticator: OAuthAuthenticator<
export namespace atlassianSignInResolvers {
const usernameMatchingUserEntityName: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
@@ -20,6 +20,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the Atlassian auth provider.
@@ -31,7 +32,12 @@ export namespace atlassianSignInResolvers {
* Looks up the user by matching their Atlassian username to the entity name.
*/
export const usernameMatchingUserEntityName = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -43,7 +49,15 @@ export namespace atlassianSignInResolvers {
throw new Error(`Atlassian user profile does not contain a username`);
}
return ctx.signInWithCatalogUser({ entityRef: { name: id } });
return ctx.signInWithCatalogUser(
{ entityRef: { name: id } },
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: id } }
: undefined,
},
);
};
},
});
+5 -1
View File
@@ -46,8 +46,12 @@ export interface Config {
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -42,7 +42,8 @@
"@backstage/plugin-auth-backend": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"jose": "^5.0.0",
"node-cache": "^5.1.2"
"node-cache": "^5.1.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
@@ -41,7 +41,10 @@ export namespace awsAlbSignInResolvers {
const // (undocumented)
emailMatchingUserEntityProfileEmail: SignInResolverFactory<
AwsAlbResult,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -19,6 +19,8 @@ import {
SignInInfo,
} from '@backstage/plugin-auth-node';
import { AwsAlbResult } from './types';
import { z } from 'zod';
/**
* Available sign-in resolvers for the AWS ALB auth provider.
*
@@ -27,19 +29,37 @@ import { AwsAlbResult } from './types';
export namespace awsAlbSignInResolvers {
export const emailMatchingUserEntityProfileEmail =
createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (info: SignInInfo<AwsAlbResult>, ctx) => {
if (!info.result.fullProfile.emails) {
throw new Error(
'Login failed, user profile does not contain an email',
);
}
return ctx.signInWithCatalogUser({
filter: {
kind: ['User'],
'spec.profile.email': info.result.fullProfile.emails[0].value,
return ctx.signInWithCatalogUser(
{
filter: {
kind: ['User'],
'spec.profile.email': info.result.fullProfile.emails[0].value,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? {
entityRef: {
name: info.result.fullProfile.emails[0].value,
},
}
: undefined,
},
);
};
},
});
@@ -40,7 +40,8 @@
"@types/passport": "^1.0.16",
"express": "^4.19.2",
"jose": "^5.0.0",
"passport": "^0.7.0"
"passport": "^0.7.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
@@ -32,7 +32,10 @@ export namespace azureEasyAuthSignInResolvers {
const // (undocumented)
idMatchingUserEntityAnnotation: SignInResolverFactory<
AzureEasyAuthResult,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
@@ -19,11 +19,17 @@ import {
SignInInfo,
} from '@backstage/plugin-auth-node';
import { AzureEasyAuthResult } from './types';
import { z } from 'zod';
/** @public */
export namespace azureEasyAuthSignInResolvers {
export const idMatchingUserEntityAnnotation = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (info: SignInInfo<AzureEasyAuthResult>, ctx) => {
const {
fullProfile: { id },
@@ -32,12 +38,19 @@ export namespace azureEasyAuthSignInResolvers {
if (!id) {
throw new Error('User profile contained no id');
}
return await ctx.signInWithCatalogUser({
annotations: {
'graph.microsoft.com/user-id': id,
return ctx.signInWithCatalogUser(
{
annotations: {
'graph.microsoft.com/user-id': id,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: id } }
: undefined,
},
);
};
},
});
+13 -2
View File
@@ -30,12 +30,23 @@ export interface Config {
additionalScopes?: string | string[];
signIn?: {
resolvers: Array<
| { resolver: 'userIdMatchingUserEntityAnnotation' }
| {
resolver: 'userIdMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'usernameMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -38,7 +38,8 @@
"@backstage/plugin-auth-node": "workspace:^",
"express": "^4.18.2",
"passport": "^0.7.0",
"passport-bitbucket-oauth2": "^0.1.2"
"passport-bitbucket-oauth2": "^0.1.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -24,11 +24,17 @@ export const bitbucketAuthenticator: OAuthAuthenticator<
export namespace bitbucketSignInResolvers {
const userIdMatchingUserEntityAnnotation: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
const usernameMatchingUserEntityAnnotation: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -20,6 +20,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the Bitbucket auth provider.
@@ -32,7 +33,12 @@ export namespace bitbucketSignInResolvers {
*/
export const userIdMatchingUserEntityAnnotation = createSignInResolverFactory(
{
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -44,11 +50,19 @@ export namespace bitbucketSignInResolvers {
throw new Error('Bitbucket user profile does not contain an ID');
}
return ctx.signInWithCatalogUser({
annotations: {
'bitbucket.org/user-id': id,
return ctx.signInWithCatalogUser(
{
annotations: {
'bitbucket.org/user-id': id,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: id } }
: undefined,
},
);
};
},
},
@@ -59,7 +73,12 @@ export namespace bitbucketSignInResolvers {
*/
export const usernameMatchingUserEntityAnnotation =
createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -73,11 +92,19 @@ export namespace bitbucketSignInResolvers {
);
}
return ctx.signInWithCatalogUser({
annotations: {
'bitbucket.org/username': username,
return ctx.signInWithCatalogUser(
{
annotations: {
'bitbucket.org/username': username,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: username } }
: undefined,
},
);
};
},
});
@@ -29,6 +29,27 @@ export interface Config {
clientSecret: string;
host: string;
callbackUrl?: string;
signIn?: {
resolvers: Array<
| {
resolver: 'userIdMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'usernameMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
>;
};
sessionDuration?: HumanDuration | string;
};
};
@@ -37,7 +37,8 @@
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"passport": "^0.7.0",
"passport-oauth2": "^1.6.1"
"passport-oauth2": "^1.6.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -27,7 +27,10 @@ export const bitbucketServerAuthenticator: OAuthAuthenticator<
export namespace bitbucketServerSignInResolvers {
const emailMatchingUserEntityProfileEmail: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -19,6 +19,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the Bitbucket Server auth provider.
@@ -31,7 +32,12 @@ export namespace bitbucketServerSignInResolvers {
*/
export const emailMatchingUserEntityProfileEmail =
createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -44,11 +50,19 @@ export namespace bitbucketServerSignInResolvers {
);
}
return ctx.signInWithCatalogUser({
filter: {
'spec.profile.email': profile.email,
return ctx.signInWithCatalogUser(
{
filter: {
'spec.profile.email': profile.email,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: profile.email } }
: undefined,
},
);
};
},
});
@@ -34,8 +34,12 @@ export interface Config {
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
};
@@ -39,7 +39,8 @@
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"express": "^4.18.2",
"jose": "^5.0.0"
"jose": "^5.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -52,7 +52,10 @@ export type CloudflareAccessResult = {
export namespace cloudflareAccessSignInResolvers {
const emailMatchingUserEntityProfileEmail: SignInResolverFactory<
CloudflareAccessResult,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
@@ -37,8 +37,13 @@ describe('resolvers', () => {
} satisfies Partial<AuthResolverContext>;
await resolver(info, context as any);
expect(context.signInWithCatalogUser).toHaveBeenCalledWith({
filter: { 'spec.profile.email': 'hello@example.com' },
});
expect(context.signInWithCatalogUser).toHaveBeenCalledWith(
{
filter: { 'spec.profile.email': 'hello@example.com' },
},
{
dangerousEntityRefFallback: undefined,
},
);
});
});
@@ -19,6 +19,7 @@ import {
SignInInfo,
} from '@backstage/plugin-auth-node';
import { CloudflareAccessResult } from './types';
import { z } from 'zod';
/**
* Available sign-in resolvers for the Cloudflare Access auth provider.
@@ -31,7 +32,12 @@ export namespace cloudflareAccessSignInResolvers {
*/
export const emailMatchingUserEntityProfileEmail =
createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (info: SignInInfo<CloudflareAccessResult>, ctx) => {
const { profile } = info;
@@ -41,11 +47,19 @@ export namespace cloudflareAccessSignInResolvers {
);
}
return ctx.signInWithCatalogUser({
filter: {
'spec.profile.email': profile.email,
return ctx.signInWithCatalogUser(
{
filter: {
'spec.profile.email': profile.email,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: profile.email } }
: undefined,
},
);
};
},
});
+13 -3
View File
@@ -36,13 +36,23 @@ export interface Config {
signIn?: {
resolvers: Array<
| { resolver: 'emailMatchingUserEntityAnnotation' }
| { resolver: 'idMatchingUserEntityAnnotation' }
| {
resolver: 'emailMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'idMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -42,7 +42,8 @@
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/types": "workspace:^",
"google-auth-library": "^9.0.0"
"google-auth-library": "^9.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
@@ -35,11 +35,17 @@ export type GcpIapResult = {
export namespace gcpIapSignInResolvers {
const emailMatchingUserEntityAnnotation: SignInResolverFactory<
GcpIapResult,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
const idMatchingUserEntityAnnotation: SignInResolverFactory<
GcpIapResult,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
@@ -19,6 +19,7 @@ import {
SignInInfo,
} from '@backstage/plugin-auth-node';
import { GcpIapResult } from './types';
import { z } from 'zod';
/**
* Available sign-in resolvers for the Google auth provider.
@@ -30,7 +31,12 @@ export namespace gcpIapSignInResolvers {
* Looks up the user by matching their email to the `google.com/email` annotation.
*/
export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (info: SignInInfo<GcpIapResult>, ctx) => {
const email = info.result.iapToken.email;
@@ -38,11 +44,19 @@ export namespace gcpIapSignInResolvers {
throw new Error('Google IAP sign-in result is missing email');
}
return ctx.signInWithCatalogUser({
annotations: {
'google.com/email': email,
return ctx.signInWithCatalogUser(
{
annotations: {
'google.com/email': email,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: email } }
: undefined,
},
);
};
},
});
@@ -51,15 +65,28 @@ export namespace gcpIapSignInResolvers {
* Looks up the user by matching their user ID to the `google.com/user-id` annotation.
*/
export const idMatchingUserEntityAnnotation = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (info: SignInInfo<GcpIapResult>, ctx) => {
const userId = info.result.iapToken.sub.split(':')[1];
return ctx.signInWithCatalogUser({
annotations: {
'google.com/user-id': userId,
return ctx.signInWithCatalogUser(
{
annotations: {
'google.com/user-id': userId,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: userId } }
: undefined,
},
);
};
},
});
+10 -4
View File
@@ -32,12 +32,18 @@ export interface Config {
additionalScopes?: string | string[];
signIn?: {
resolvers: Array<
| { resolver: 'usernameMatchingUserEntityName' }
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
resolver: 'usernameMatchingUserEntityName';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'preferredUsernameMatchingUserEntityName';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -36,7 +36,8 @@
"dependencies": {
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"passport-github2": "^0.1.12"
"passport-github2": "^0.1.12",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -24,7 +24,10 @@ export const githubAuthenticator: OAuthAuthenticator<
export namespace githubSignInResolvers {
const usernameMatchingUserEntityName: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -20,6 +20,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the GitHub auth provider.
@@ -31,7 +32,12 @@ export namespace githubSignInResolvers {
* Looks up the user by matching their GitHub username to the entity name.
*/
export const usernameMatchingUserEntityName = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -43,7 +49,17 @@ export namespace githubSignInResolvers {
throw new Error(`GitHub user profile does not contain a username`);
}
return ctx.signInWithCatalogUser({ entityRef: { name: userId } });
return ctx.signInWithCatalogUser(
{
entityRef: { name: userId },
},
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: userId } }
: undefined,
},
);
};
},
});
+9 -2
View File
@@ -32,12 +32,19 @@ export interface Config {
additionalScopes?: string | string[];
signIn?: {
resolvers: Array<
| { resolver: 'usernameMatchingUserEntityName' }
| {
resolver: 'usernameMatchingUserEntityName';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -38,7 +38,8 @@
"@backstage/plugin-auth-node": "workspace:^",
"express": "^4.18.2",
"passport": "^0.7.0",
"passport-gitlab2": "^5.0.0"
"passport-gitlab2": "^5.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -24,7 +24,10 @@ export const gitlabAuthenticator: OAuthAuthenticator<
export namespace gitlabSignInResolvers {
const usernameMatchingUserEntityName: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -20,6 +20,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the GitLab auth provider.
@@ -31,7 +32,12 @@ export namespace gitlabSignInResolvers {
* Looks up the user by matching their GitLab username to the entity name.
*/
export const usernameMatchingUserEntityName = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -43,7 +49,17 @@ export namespace gitlabSignInResolvers {
throw new Error(`GitLab user profile does not contain a username`);
}
return ctx.signInWithCatalogUser({ entityRef: { name: id } });
return ctx.signInWithCatalogUser(
{
entityRef: { name: id },
},
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: id } }
: undefined,
},
);
};
},
});
+9 -2
View File
@@ -32,12 +32,19 @@ export interface Config {
additionalScopes?: string | string[];
signIn?: {
resolvers: Array<
| { resolver: 'emailMatchingUserEntityAnnotation' }
| {
resolver: 'emailMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -41,7 +41,8 @@
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"google-auth-library": "^9.0.0",
"passport-google-oauth20": "^2.0.0"
"passport-google-oauth20": "^2.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
@@ -24,7 +24,10 @@ export const googleAuthenticator: OAuthAuthenticator<
export namespace googleSignInResolvers {
const emailMatchingUserEntityAnnotation: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
@@ -20,6 +20,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the Google auth provider.
@@ -31,7 +32,12 @@ export namespace googleSignInResolvers {
* Looks up the user by matching their email to the `google.com/email` annotation.
*/
export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -42,11 +48,19 @@ export namespace googleSignInResolvers {
throw new Error('Google profile contained no email');
}
return ctx.signInWithCatalogUser({
annotations: {
'google.com/email': profile.email,
return ctx.signInWithCatalogUser(
{
annotations: {
'google.com/email': profile.email,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: profile.email } }
: undefined,
},
);
};
},
});
+13 -3
View File
@@ -34,13 +34,23 @@ export interface Config {
skipUserProfile?: boolean;
signIn?: {
resolvers: Array<
| { resolver: 'emailMatchingUserEntityAnnotation' }
| {
resolver: 'emailMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'userIdMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
| { resolver: 'userIdMatchingUserEntityAnnotation' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -38,7 +38,8 @@
"@backstage/plugin-auth-node": "workspace:^",
"express": "^4.18.2",
"jose": "^5.0.0",
"passport-microsoft": "^1.0.0"
"passport-microsoft": "^1.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -30,11 +30,17 @@ export const microsoftAuthenticator: OAuthAuthenticator<
export namespace microsoftSignInResolvers {
const emailMatchingUserEntityAnnotation: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
const userIdMatchingUserEntityAnnotation: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -20,6 +20,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the Microsoft auth provider.
@@ -31,7 +32,12 @@ export namespace microsoftSignInResolvers {
* Looks up the user by matching their Microsoft email to the email entity annotation.
*/
export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -42,11 +48,19 @@ export namespace microsoftSignInResolvers {
throw new Error('Microsoft profile contained no email');
}
return ctx.signInWithCatalogUser({
annotations: {
'microsoft.com/email': profile.email,
return ctx.signInWithCatalogUser(
{
annotations: {
'microsoft.com/email': profile.email,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: profile.email } }
: undefined,
},
);
};
},
});
@@ -55,7 +69,12 @@ export namespace microsoftSignInResolvers {
*/
export const userIdMatchingUserEntityAnnotation = createSignInResolverFactory(
{
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -68,11 +87,19 @@ export namespace microsoftSignInResolvers {
throw new Error('Microsoft profile contained no id');
}
return ctx.signInWithCatalogUser({
annotations: {
'graph.microsoft.com/user-id': id,
return ctx.signInWithCatalogUser(
{
annotations: {
'graph.microsoft.com/user-id': id,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: id } }
: undefined,
},
);
};
},
},
+9 -2
View File
@@ -36,12 +36,19 @@ export interface Config {
includeBasicAuth?: boolean;
signIn?: {
resolvers: Array<
| { resolver: 'usernameMatchingUserEntityName' }
| {
resolver: 'usernameMatchingUserEntityName';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -37,7 +37,8 @@
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"passport": "^0.7.0",
"passport-oauth2": "^1.6.1"
"passport-oauth2": "^1.6.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -24,7 +24,10 @@ export const oauth2Authenticator: OAuthAuthenticator<
export namespace oauth2SignInResolvers {
const usernameMatchingUserEntityName: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -20,6 +20,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the oauth2 auth provider.
@@ -31,7 +32,12 @@ export namespace oauth2SignInResolvers {
* Looks up the user by matching their oauth2 username to the entity name.
*/
export const usernameMatchingUserEntityName = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -43,7 +49,17 @@ export namespace oauth2SignInResolvers {
throw new Error(`Oauth2 user profile does not contain a username`);
}
return ctx.signInWithCatalogUser({ entityRef: { name: id } });
return ctx.signInWithCatalogUser(
{
entityRef: { name: id },
},
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: id } }
: undefined,
},
);
};
},
});
@@ -36,7 +36,8 @@
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"jose": "^5.0.0"
"jose": "^5.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-test-utils": "workspace:^",
@@ -6,6 +6,7 @@
import { BackendFeature } from '@backstage/backend-plugin-api';
import { IncomingHttpHeaders } from 'http';
import { ProxyAuthenticator } from '@backstage/plugin-auth-node';
import { SignInResolverFactory } from '@backstage/plugin-auth-node';
// @public (undocumented)
const authModuleOauth2ProxyProvider: BackendFeature;
@@ -30,4 +31,16 @@ export type OAuth2ProxyResult<JWTPayload = {}> = {
headers: IncomingHttpHeaders;
getHeader(name: string): string | undefined;
};
// @public (undocumented)
export namespace oauth2ProxySignInResolvers {
const // (undocumented)
forwardedUserMatchingUserEntityName: SignInResolverFactory<
OAuth2ProxyResult,
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -20,6 +20,7 @@
* @packageDocumentation
*/
export { authModuleOauth2ProxyProvider as default } from './module';
export { oauth2ProxySignInResolvers } from './resolvers';
export {
oauth2ProxyAuthenticator,
OAUTH2_PROXY_JWT_HEADER,
@@ -19,6 +19,7 @@ import {
SignInInfo,
} from '@backstage/plugin-auth-node';
import { OAuth2ProxyResult } from './types';
import { z } from 'zod';
/**
* @public
@@ -26,15 +27,29 @@ import { OAuth2ProxyResult } from './types';
export namespace oauth2ProxySignInResolvers {
export const forwardedUserMatchingUserEntityName =
createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (info: SignInInfo<OAuth2ProxyResult>, ctx) => {
const name = info.result.getHeader('x-forwarded-user');
if (!name) {
throw new Error('Request did not contain a user');
}
return ctx.signInWithCatalogUser({
entityRef: { name },
});
return ctx.signInWithCatalogUser(
{
entityRef: { name },
},
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name } }
: undefined,
},
);
};
},
});
+9 -1
View File
@@ -38,8 +38,16 @@ export interface Config {
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'preferredUsernameMatchingUserEntityName';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -41,7 +41,8 @@
"@backstage/types": "workspace:^",
"express": "^4.18.2",
"openid-client": "^5.5.0",
"passport": "^0.7.0"
"passport": "^0.7.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -41,12 +41,17 @@ export namespace oidcSignInResolvers {
unknown,
| {
allowedDomains?: string[] | undefined;
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
const emailMatchingUserEntityProfileEmail: SignInResolverFactory<
unknown,
unknown
| {
allowedDomains?: string[] | undefined;
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -16,7 +16,6 @@
import { createBackendModule } from '@backstage/backend-plugin-api';
import {
authProvidersExtensionPoint,
commonSignInResolvers,
createOAuthProviderFactory,
} from '@backstage/plugin-auth-node';
import { oidcAuthenticator } from './authenticator';
@@ -38,7 +37,6 @@ export const authModuleOidcProvider = createBackendModule({
authenticator: oidcAuthenticator,
signInResolverFactories: {
...oidcSignInResolvers,
...commonSignInResolvers,
},
}),
});
+9 -2
View File
@@ -34,12 +34,19 @@ export interface Config {
additionalScopes?: string | string[];
signIn?: {
resolvers: Array<
| { resolver: 'emailMatchingUserEntityAnnotation' }
| {
resolver: 'emailMatchingUserEntityAnnotation';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -38,7 +38,8 @@
"@backstage/plugin-auth-node": "workspace:^",
"@davidzemon/passport-okta-oauth": "^0.0.5",
"express": "^4.18.2",
"passport": "^0.7.0"
"passport": "^0.7.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -24,7 +24,10 @@ export const oktaAuthenticator: OAuthAuthenticator<
export namespace oktaSignInResolvers {
const emailMatchingUserEntityAnnotation: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -20,6 +20,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the Okta auth provider.
@@ -32,7 +33,12 @@ export namespace oktaSignInResolvers {
*/
export const emailMatchingUserEntityAnnotation = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -43,11 +49,19 @@ export namespace oktaSignInResolvers {
throw new Error('Okta profile contained no email');
}
return ctx.signInWithCatalogUser({
annotations: {
'okta.com/email': profile.email,
return ctx.signInWithCatalogUser(
{
annotations: {
'okta.com/email': profile.email,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: profile.email } }
: undefined,
},
);
};
},
});
+9 -2
View File
@@ -31,12 +31,19 @@ export interface Config {
callbackUrl?: string;
signIn?: {
resolvers: Array<
| { resolver: 'usernameMatchingUserEntityName' }
| {
resolver: 'usernameMatchingUserEntityName';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -38,7 +38,8 @@
"@backstage/plugin-auth-node": "workspace:^",
"express": "^4.18.2",
"passport": "^0.7.0",
"passport-onelogin-oauth": "^0.0.1"
"passport-onelogin-oauth": "^0.0.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@backstage/backend-defaults": "workspace:^",
@@ -24,7 +24,10 @@ export const oneLoginAuthenticator: OAuthAuthenticator<
export namespace oneLoginSignInResolvers {
const usernameMatchingUserEntityName: SignInResolverFactory<
OAuthAuthenticatorResult<PassportProfile>,
unknown
| {
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
}
```
@@ -20,6 +20,7 @@ import {
PassportProfile,
SignInInfo,
} from '@backstage/plugin-auth-node';
import { z } from 'zod';
/**
* Available sign-in resolvers for the OneLogin auth provider.
@@ -31,7 +32,12 @@ export namespace oneLoginSignInResolvers {
* Looks up the user by matching their OneLogin username to the entity name.
*/
export const usernameMatchingUserEntityName = createSignInResolverFactory({
create() {
optionsSchema: z
.object({
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
ctx,
@@ -43,7 +49,17 @@ export namespace oneLoginSignInResolvers {
throw new Error(`OneLogin user profile does not contain a username`);
}
return ctx.signInWithCatalogUser({ entityRef: { name: id } });
return ctx.signInWithCatalogUser(
{
entityRef: { name: id },
},
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: id } }
: undefined,
},
);
};
},
});
@@ -32,8 +32,12 @@ export interface Config {
| {
resolver: 'emailLocalPartMatchingUserEntityName';
allowedDomains?: string[];
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| {
resolver: 'emailMatchingUserEntityProfileEmail';
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
}
| { resolver: 'emailMatchingUserEntityProfileEmail' }
>;
};
sessionDuration?: HumanDuration | string;
@@ -139,19 +139,54 @@ export class CatalogAuthResolverContext implements AuthResolverContext {
return { entity: result };
}
async signInWithCatalogUser(query: AuthResolverCatalogUserQuery) {
const { entity } = await this.findCatalogUser(query);
async signInWithCatalogUser(
query: AuthResolverCatalogUserQuery,
options?: {
dangerousEntityRefFallback?: {
entityRef:
| string
| {
kind?: string;
namespace?: string;
name: string;
};
};
},
) {
try {
const { entity } = await this.findCatalogUser(query);
const { ownershipEntityRefs } = await this.resolveOwnershipEntityRefs(
entity,
);
const { ownershipEntityRefs } = await this.resolveOwnershipEntityRefs(
entity,
);
return await this.tokenIssuer.issueToken({
claims: {
sub: stringifyEntityRef(entity),
ent: ownershipEntityRefs,
},
});
return await this.tokenIssuer.issueToken({
claims: {
sub: stringifyEntityRef(entity),
ent: ownershipEntityRefs,
},
});
} catch (error) {
if (
error?.name !== 'NotFoundError' ||
!options?.dangerousEntityRefFallback
) {
throw error;
}
const userEntityRef = stringifyEntityRef(
parseEntityRef(options.dangerousEntityRefFallback.entityRef, {
defaultKind: 'User',
defaultNamespace: DEFAULT_NAMESPACE,
}),
);
return await this.tokenIssuer.issueToken({
claims: {
sub: userEntityRef,
ent: [userEntityRef],
},
});
}
}
async resolveOwnershipEntityRefs(
+17 -1
View File
@@ -110,6 +110,17 @@ export type AuthResolverContext = {
}>;
signInWithCatalogUser(
query: AuthResolverCatalogUserQuery,
options?: {
dangerousEntityRefFallback?: {
entityRef:
| string
| {
kind?: string;
namespace?: string;
name: string;
};
};
},
): Promise<BackstageSignInResult>;
resolveOwnershipEntityRefs(entity: Entity): Promise<{
ownershipEntityRefs: string[];
@@ -146,12 +157,17 @@ export type ClientAuthResponse<TProviderInfo> = {
export namespace commonSignInResolvers {
const emailMatchingUserEntityProfileEmail: SignInResolverFactory<
unknown,
unknown
| {
allowedDomains?: string[] | undefined;
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
const emailLocalPartMatchingUserEntityName: SignInResolverFactory<
unknown,
| {
allowedDomains?: string[] | undefined;
dangerouslyAllowSignInWithoutUserInCatalog?: boolean | undefined;
}
| undefined
>;
@@ -35,7 +35,13 @@ export namespace commonSignInResolvers {
*/
export const emailMatchingUserEntityProfileEmail =
createSignInResolverFactory({
create() {
optionsSchema: z
.object({
allowedDomains: z.array(z.string()).optional(),
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
return async (info, ctx) => {
const { profile } = info;
@@ -59,11 +65,19 @@ export namespace commonSignInResolvers {
const [_, name, _plus, domain] = m;
const noPlusEmail = `${name}${domain}`;
return ctx.signInWithCatalogUser({
filter: {
'spec.profile.email': noPlusEmail,
return ctx.signInWithCatalogUser(
{
filter: {
'spec.profile.email': noPlusEmail,
},
},
});
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: noPlusEmail } }
: undefined,
},
);
}
}
// Email had no plus addressing or is missing in the catalog, forward failure
@@ -82,6 +96,7 @@ export namespace commonSignInResolvers {
optionsSchema: z
.object({
allowedDomains: z.array(z.string()).optional(),
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
})
.optional(),
create(options = {}) {
@@ -102,10 +117,15 @@ export namespace commonSignInResolvers {
'Sign-in user email is not from an allowed domain',
);
}
return ctx.signInWithCatalogUser({
entityRef: { name: localPart },
});
return ctx.signInWithCatalogUser(
{ entityRef: { name: localPart } },
{
dangerousEntityRefFallback:
options?.dangerouslyAllowSignInWithoutUserInCatalog
? { entityRef: { name: localPart } }
: undefined,
},
);
};
},
});
+16
View File
@@ -162,10 +162,26 @@ export type AuthResolverContext = {
* Finds a single user in the catalog using the provided query, and then
* issues an identity for that user using default ownership resolution.
*
* If the user is not found, an optional `dangerousEntityRefFallback`
* entity ref can be provided to allow sign-in to proceed by issuing an
* identity based on the given ref. This bypasses the requirement for the
* user to exist in the catalog and should be used with caution.
*
* See {@link AuthResolverCatalogUserQuery} for details.
*/
signInWithCatalogUser(
query: AuthResolverCatalogUserQuery,
options?: {
dangerousEntityRefFallback?: {
entityRef:
| string
| {
kind?: string;
namespace?: string;
name: string;
};
};
},
): Promise<BackstageSignInResult>;
/**
+16
View File
@@ -5275,6 +5275,7 @@ __metadata:
passport: "npm:^0.7.0"
passport-atlassian-oauth2: "npm:^2.1.0"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5314,6 +5315,7 @@ __metadata:
jose: "npm:^5.0.0"
msw: "npm:^2.0.8"
node-cache: "npm:^5.1.2"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5332,6 +5334,7 @@ __metadata:
express: "npm:^4.19.2"
jose: "npm:^5.0.0"
passport: "npm:^0.7.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5350,6 +5353,7 @@ __metadata:
passport: "npm:^0.7.0"
passport-bitbucket-oauth2: "npm:^0.1.2"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5368,6 +5372,7 @@ __metadata:
passport: "npm:^0.7.0"
passport-oauth2: "npm:^1.6.1"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5389,6 +5394,7 @@ __metadata:
msw: "npm:^2.0.0"
node-mocks-http: "npm:^1.0.0"
uuid: "npm:^11.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5404,6 +5410,7 @@ __metadata:
"@backstage/types": "workspace:^"
express: "npm:^4.18.2"
google-auth-library: "npm:^9.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5421,6 +5428,7 @@ __metadata:
"@types/passport-github2": "npm:^1.2.4"
passport-github2: "npm:^0.1.12"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5439,6 +5447,7 @@ __metadata:
passport: "npm:^0.7.0"
passport-gitlab2: "npm:^5.0.0"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5456,6 +5465,7 @@ __metadata:
google-auth-library: "npm:^9.0.0"
passport-google-oauth20: "npm:^2.0.0"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5493,6 +5503,7 @@ __metadata:
msw: "npm:^1.0.0"
passport-microsoft: "npm:^1.0.0"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5510,6 +5521,7 @@ __metadata:
passport: "npm:^0.7.0"
passport-oauth2: "npm:^1.6.1"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5523,6 +5535,7 @@ __metadata:
"@backstage/errors": "workspace:^"
"@backstage/plugin-auth-node": "workspace:^"
jose: "npm:^5.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5547,6 +5560,7 @@ __metadata:
openid-client: "npm:^5.5.0"
passport: "npm:^0.7.0"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5565,6 +5579,7 @@ __metadata:
express: "npm:^4.18.2"
passport: "npm:^0.7.0"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft
@@ -5583,6 +5598,7 @@ __metadata:
passport: "npm:^0.7.0"
passport-onelogin-oauth: "npm:^0.0.1"
supertest: "npm:^7.0.0"
zod: "npm:^3.22.4"
languageName: unknown
linkType: soft