diff --git a/.changeset/honest-apples-notice.md b/.changeset/honest-apples-notice.md new file mode 100644 index 0000000000..da517b95a7 --- /dev/null +++ b/.changeset/honest-apples-notice.md @@ -0,0 +1,18 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +**BREAKING**: The default sign-in resolvers for all providers, if you choose to +use them, now emit the token `sub` and `ent` claims on the standard, +all-lowercase form, instead of the mixed-case form. The mixed-case form causes +problems for implementations that naively do string comparisons on refs. The end +result is that you may for example see your Backstage token `sub` claim now +become `'user:default/my-id'` instead of `'user:default/My-ID'`. + +On a related note, specifically the SAML provider now correctly issues both +`sub` and `ent` claims, and on the full entity ref form instead of the short +form with only the ID. + +**NOTE**: For a long time, it has been strongly recommended that you provide +your own sign-in resolver instead of using the builtin ones, and that will +become mandatory in the future. diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index ec7f8cafa6..29e8679fe0 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { Logger } from 'winston'; import { Profile as PassportProfile } from 'passport'; @@ -247,10 +251,16 @@ export const githubDefaultSignInResolver: SignInResolver< const userId = fullProfile.username || fullProfile.id; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ claims: { - sub: `user:default/${userId}`, - ent: [`user:default/${userId}`], + sub: entityRef, + ent: [entityRef], }, }); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 6689aba884..59f9a5a4cb 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -14,10 +14,13 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { Strategy as GitlabStrategy } from 'passport-gitlab2'; import { Logger } from 'winston'; - import { executeRedirectStrategy, executeFrameHandlerStrategy, @@ -71,8 +74,17 @@ export const gitlabDefaultSignInResolver: SignInResolver = async ( id = profile.email.split('@')[0]; } + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: `user:default/${id}`, ent: [`user:default/${id}`] }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id, token }; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index fbaf8c7462..e5e5698ec4 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; @@ -226,8 +230,17 @@ const googleDefaultSignInResolver: SignInResolver = async ( userId = profile.email.split('@')[0]; } + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id: userId, token }; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index 25c940547b..98f94811bd 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as MicrosoftStrategy } from 'passport-microsoft'; @@ -231,10 +235,16 @@ export const microsoftDefaultSignInResolver: SignInResolver< const userId = profile.email.split('@')[0]; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ claims: { - sub: `user:default/${userId}`, - ent: [`user:default/${userId}`], + sub: entityRef, + ent: [entityRef], }, }); diff --git a/plugins/auth-backend/src/providers/oauth2/index.ts b/plugins/auth-backend/src/providers/oauth2/index.ts index 767466f8f6..d68208a148 100644 --- a/plugins/auth-backend/src/providers/oauth2/index.ts +++ b/plugins/auth-backend/src/providers/oauth2/index.ts @@ -15,5 +15,4 @@ */ export { createOAuth2Provider } from './provider'; - export type { OAuth2ProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index 0906005791..d09337f5ad 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import passport from 'passport'; import { Strategy as OAuth2Strategy } from 'passport-oauth2'; @@ -210,8 +214,17 @@ export const oAuth2DefaultSignInResolver: SignInResolver = async ( const userId = profile.email.split('@')[0]; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id: userId, token }; diff --git a/plugins/auth-backend/src/providers/oidc/index.ts b/plugins/auth-backend/src/providers/oidc/index.ts index 39bd02928d..e63c9920df 100644 --- a/plugins/auth-backend/src/providers/oidc/index.ts +++ b/plugins/auth-backend/src/providers/oidc/index.ts @@ -13,5 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export type { OidcAuthResult, OidcProviderOptions } from './provider'; export { createOidcProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 9ccdf25f74..b4f34cd127 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { Client, @@ -211,21 +215,31 @@ export class OidcAuthProvider implements OAuthHandlers { } } -export const oAuth2DefaultSignInResolver: SignInResolver< - OidcAuthResult -> = async (info, ctx) => { +export const oidcDefaultSignInResolver: SignInResolver = async ( + info, + ctx, +) => { const { profile } = info; if (!profile.email) { throw new Error('Profile contained no email'); } + const userId = profile.email.split('@')[0]; + + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ claims: { - sub: `user:default/${userId}`, - ent: [`user:default/${userId}`], + sub: entityRef, + ent: [entityRef], }, }); + return { id: userId, token }; }; @@ -289,7 +303,7 @@ export const createOidcProvider = ( }, }); const signInResolverFn = - options?.signIn?.resolver ?? oAuth2DefaultSignInResolver; + options?.signIn?.resolver ?? oidcDefaultSignInResolver; const signInResolver: SignInResolver = info => signInResolverFn(info, { catalogIdentityClient, diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 0ba7d241d3..26f586c578 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -13,6 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { OAuthAdapter, @@ -235,8 +240,17 @@ export const oktaDefaultSignInResolver: SignInResolver = async ( // TODO(Rugvip): Hardcoded to the local part of the email for now const userId = profile.email.split('@')[0]; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: userId, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: `user:default/${userId}`, ent: [`user:default/${userId}`] }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id: userId, token }; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 1ca8cf0d5d..4458d423ef 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + DEFAULT_NAMESPACE, + stringifyEntityRef, +} from '@backstage/catalog-model'; import express from 'express'; import { SamlConfig } from 'passport-saml/lib/passport-saml/types'; import { @@ -150,8 +154,17 @@ const samlDefaultSignInResolver: SignInResolver = async ( ) => { const id = info.result.fullProfile.nameID; + const entityRef = stringifyEntityRef({ + kind: 'User', + namespace: DEFAULT_NAMESPACE, + name: id, + }); + const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: id }, + claims: { + sub: entityRef, + ent: [entityRef], + }, }); return { id, token };