fix casing of sub and ent claims in default sign-in resolvers

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2022-02-17 12:07:48 +01:00
parent f7fc46c746
commit 3884bf0348
11 changed files with 134 additions and 17 deletions
+18
View File
@@ -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.
@@ -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],
},
});
@@ -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<OAuthResult> = 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 };
@@ -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<OAuthResult> = 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 };
@@ -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],
},
});
@@ -15,5 +15,4 @@
*/
export { createOAuth2Provider } from './provider';
export type { OAuth2ProviderOptions } from './provider';
@@ -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<OAuthResult> = 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 };
@@ -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';
@@ -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<OidcAuthResult> = 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<OidcAuthResult> = info =>
signInResolverFn(info, {
catalogIdentityClient,
@@ -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<OAuthResult> = 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 };
@@ -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<SamlAuthResult> = 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 };