Merge pull request #29470 from ioboi/auth-backend-module-openshift-provider
Init auth-backend-module-openshift-provider [ContribFest]
This commit is contained in:
@@ -539,6 +539,21 @@ const appPlugin: OverridableFrontendPlugin<
|
||||
params: ApiFactory<TApi, TImpl, TDeps>,
|
||||
) => ExtensionBlueprintParams<AnyApiFactory>;
|
||||
}>;
|
||||
'api:app/openshift-auth': ExtensionDefinition<{
|
||||
kind: 'api';
|
||||
name: 'openshift-auth';
|
||||
config: {};
|
||||
configInput: {};
|
||||
output: ExtensionDataRef<AnyApiFactory, 'core.api.factory', {}>;
|
||||
inputs: {};
|
||||
params: <
|
||||
TApi,
|
||||
TImpl extends TApi,
|
||||
TDeps extends { [name in string]: unknown },
|
||||
>(
|
||||
params: ApiFactory<TApi, TImpl, TDeps>,
|
||||
) => ExtensionBlueprintParams<AnyApiFactory>;
|
||||
}>;
|
||||
'api:app/permission': ExtensionDefinition<{
|
||||
kind: 'api';
|
||||
name: 'permission';
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
createFetchApi,
|
||||
FetchMiddlewares,
|
||||
VMwareCloudAuth,
|
||||
OpenShiftAuth,
|
||||
} from '../../../packages/core-app-api/src/apis/implementations';
|
||||
|
||||
import {
|
||||
@@ -56,6 +57,7 @@ import {
|
||||
bitbucketServerAuthApiRef,
|
||||
atlassianAuthApiRef,
|
||||
vmwareCloudAuthApiRef,
|
||||
openshiftAuthApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { ApiBlueprint, dialogApiRef } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
@@ -353,6 +355,26 @@ export const apis = [
|
||||
},
|
||||
}),
|
||||
}),
|
||||
ApiBlueprint.make({
|
||||
name: 'openshift-auth',
|
||||
params: defineParams =>
|
||||
defineParams({
|
||||
api: openshiftAuthApiRef,
|
||||
deps: {
|
||||
discoveryApi: discoveryApiRef,
|
||||
oauthRequestApi: oauthRequestApiRef,
|
||||
configApi: configApiRef,
|
||||
},
|
||||
factory: ({ discoveryApi, oauthRequestApi, configApi }) => {
|
||||
return OpenShiftAuth.create({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
oauthRequestApi,
|
||||
environment: configApi.getOptionalString('auth.environment'),
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
ApiBlueprint.make({
|
||||
name: 'permission',
|
||||
params: defineParams =>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# @backstage/plugin-auth-backend-module-openshift-provider
|
||||
|
||||
The openshift-provider backend module for the auth plugin.
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: backstage-plugin-auth-backend-module-openshift-provider
|
||||
title: '@backstage/plugin-auth-backend-module-openshift-provider'
|
||||
description: The OpenShift backend module for the auth plugin.
|
||||
spec:
|
||||
lifecycle: experimental
|
||||
type: backstage-backend-plugin-module
|
||||
owner: auth-maintainers
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { HumanDuration } from '@backstage/types';
|
||||
|
||||
export interface Config {
|
||||
auth?: {
|
||||
providers?: {
|
||||
/** @visibility frontend */
|
||||
openshift?: {
|
||||
[authEnv: string]: {
|
||||
clientId: string;
|
||||
/**
|
||||
* @visibility secret
|
||||
*/
|
||||
clientSecret: string;
|
||||
authorizationUrl: string;
|
||||
tokenUrl: string;
|
||||
callbackUrl?: string;
|
||||
openshiftApiServerUrl: string;
|
||||
signIn?: {
|
||||
resolvers: Array<{
|
||||
resolver: 'displayNameMatchingUserEntityName';
|
||||
dangerouslyAllowSignInWithoutUserInCatalog?: boolean;
|
||||
}>;
|
||||
};
|
||||
sessionDuration?: HumanDuration | string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createBackend } from '@backstage/backend-defaults';
|
||||
import authPlugin from '@backstage/plugin-auth-backend';
|
||||
import authModuleOpenShiftProvider from '../src';
|
||||
|
||||
const backend = createBackend();
|
||||
|
||||
backend.add(authPlugin);
|
||||
backend.add(authModuleOpenShiftProvider);
|
||||
|
||||
backend.start();
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@backstage/plugin-auth-backend-module-openshift-provider",
|
||||
"version": "0.0.0",
|
||||
"description": "The OpenShift backend module for the auth plugin.",
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module",
|
||||
"pluginId": "auth",
|
||||
"pluginPackage": "@backstage/plugin-auth-backend"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/backstage/backstage",
|
||||
"directory": "plugins/auth-backend-module-openshift-provider"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"config.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "backstage-cli package build",
|
||||
"clean": "backstage-cli package clean",
|
||||
"lint": "backstage-cli package lint",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"start": "backstage-cli package start",
|
||||
"test": "backstage-cli package test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/catalog-model": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"passport-oauth2": "^1.8.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-defaults": "workspace:^",
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/plugin-auth-backend": "workspace:^",
|
||||
"express": "^4.18.2",
|
||||
"msw": "^2.7.3",
|
||||
"supertest": "^7.1.0"
|
||||
},
|
||||
"configSchema": "config.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
## API Report File for "@backstage/plugin-auth-backend-module-openshift-provider"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { OAuthAuthenticator } from '@backstage/plugin-auth-node';
|
||||
import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node';
|
||||
import { PassportProfile } from '@backstage/plugin-auth-node';
|
||||
|
||||
// @public (undocumented)
|
||||
const authModuleOpenshiftProvider: BackendFeature;
|
||||
export default authModuleOpenshiftProvider;
|
||||
|
||||
// @public (undocumented)
|
||||
export const openshiftAuthenticator: OAuthAuthenticator<
|
||||
OpenShiftAuthenticatorContext,
|
||||
PassportProfile
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface OpenShiftAuthenticatorContext {
|
||||
// (undocumented)
|
||||
helper: PassportOAuthAuthenticatorHelper;
|
||||
// (undocumented)
|
||||
openshiftApiServerUrl: string;
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { setupServer } from 'msw/node';
|
||||
import {
|
||||
decodeOAuthState,
|
||||
encodeOAuthState,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { registerMswTestHooks } from '@backstage/backend-test-utils';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { openshiftAuthenticator } from './authenticator';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
OAuthState,
|
||||
OAuthAuthenticatorStartInput,
|
||||
OAuthAuthenticatorAuthenticateInput,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import express from 'express';
|
||||
|
||||
describe('openshiftAuthenticator', () => {
|
||||
let implementation: any;
|
||||
let oauthState: OAuthState;
|
||||
|
||||
const mswServer = setupServer();
|
||||
registerMswTestHooks(mswServer);
|
||||
|
||||
beforeEach(() => {
|
||||
mswServer.use(
|
||||
http.post('https://openshift.test/oauth/token', () => {
|
||||
return HttpResponse.json({
|
||||
access_token: 'accessToken',
|
||||
scope: 'user:full',
|
||||
expires_in: 60 * 60 * 24,
|
||||
});
|
||||
}),
|
||||
http.get(
|
||||
'https://api.openshift.test/apis/user.openshift.io/v1/users/~',
|
||||
async () => {
|
||||
return HttpResponse.json({
|
||||
kind: 'User',
|
||||
apiVersion: 'user.openshift.io/v1',
|
||||
metadata: {
|
||||
name: 'alice',
|
||||
uid: 'ca993628-8817-4a3b-9811-be4a34c60bf4',
|
||||
resourceVersion: '1',
|
||||
creationTimestamp: '2022-01-11T13:10:45Z',
|
||||
managedFields: [],
|
||||
},
|
||||
fullName: 'Alice Adams',
|
||||
identities: ['SSO:id'],
|
||||
groups: ['system:authenticated', 'system:authenticated:oauth'],
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
implementation = openshiftAuthenticator.initialize({
|
||||
callbackUrl: 'https://backstage.test/callback',
|
||||
config: new ConfigReader({
|
||||
clientId: 'clientId',
|
||||
clientSecret: 'clientSecret',
|
||||
authorizationUrl: 'https://openshift.test/oauth/authorize',
|
||||
tokenUrl: 'https://openshift.test/oauth/token',
|
||||
openshiftApiServerUrl: 'https://api.openshift.test',
|
||||
}),
|
||||
});
|
||||
|
||||
oauthState = {
|
||||
nonce: 'nonce',
|
||||
env: 'env',
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('#start', () => {
|
||||
let fakeSession: Record<string, any>;
|
||||
let startRequest: OAuthAuthenticatorStartInput;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeSession = {};
|
||||
startRequest = {
|
||||
state: encodeOAuthState(oauthState),
|
||||
req: {
|
||||
method: 'GET',
|
||||
url: 'test',
|
||||
session: fakeSession,
|
||||
},
|
||||
} as unknown as OAuthAuthenticatorStartInput;
|
||||
});
|
||||
|
||||
it('initiates authorization code grant', async () => {
|
||||
const startResponse = await openshiftAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('response_type')).toBe('code');
|
||||
});
|
||||
|
||||
it('passes client ID from config', async () => {
|
||||
const startResponse = await openshiftAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('client_id')).toBe('clientId');
|
||||
});
|
||||
|
||||
it('passes callback URL from config', async () => {
|
||||
const startResponse = await openshiftAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
|
||||
expect(searchParams.get('redirect_uri')).toBe(
|
||||
'https://backstage.test/callback',
|
||||
);
|
||||
});
|
||||
|
||||
it('encodes OAuth state in query param', async () => {
|
||||
const startResponse = await openshiftAuthenticator.start(
|
||||
startRequest,
|
||||
implementation,
|
||||
);
|
||||
const { searchParams } = new URL(startResponse.url);
|
||||
const stateParam = searchParams.get('state');
|
||||
const decodedState = decodeOAuthState(stateParam!);
|
||||
|
||||
expect(decodedState).toMatchObject(oauthState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#authenticate', () => {
|
||||
let handlerRequest: OAuthAuthenticatorAuthenticateInput;
|
||||
|
||||
beforeEach(() => {
|
||||
handlerRequest = {
|
||||
req: {
|
||||
method: 'GET',
|
||||
query: {
|
||||
code: 'authorization_code',
|
||||
state: encodeOAuthState(oauthState),
|
||||
},
|
||||
session: {
|
||||
'oauth2:openshift': {
|
||||
state: encodeOAuthState(oauthState),
|
||||
},
|
||||
},
|
||||
} as unknown as express.Request,
|
||||
};
|
||||
});
|
||||
|
||||
it('exchanges authorization code for access token', async () => {
|
||||
const authenticatorResult = await openshiftAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const accessToken = authenticatorResult.session.accessToken;
|
||||
|
||||
expect(accessToken).toEqual('accessToken');
|
||||
});
|
||||
|
||||
it('returns granted scope', async () => {
|
||||
const authenticatorResult = await openshiftAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const responseScope = authenticatorResult.session.scope;
|
||||
|
||||
expect(responseScope).toEqual('user:full');
|
||||
});
|
||||
|
||||
it('returns a default session.tokentype field', async () => {
|
||||
const authenticatorResult = await openshiftAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
const tokenType = authenticatorResult.session.tokenType;
|
||||
|
||||
expect(tokenType).toEqual('bearer');
|
||||
});
|
||||
|
||||
it('returns displayName', async () => {
|
||||
const authenticatorResult = await openshiftAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
|
||||
expect(authenticatorResult).toMatchObject({
|
||||
fullProfile: {
|
||||
displayName: 'alice',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should store access token as refresh token', async () => {
|
||||
const authenticatorResult = await openshiftAuthenticator.authenticate(
|
||||
handlerRequest,
|
||||
implementation,
|
||||
);
|
||||
|
||||
expect(authenticatorResult.session.refreshToken).toBe(
|
||||
authenticatorResult.session.accessToken,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createOAuthAuthenticator,
|
||||
PassportOAuthAuthenticatorHelper,
|
||||
PassportOAuthDoneCallback,
|
||||
PassportProfile,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { createHash } from 'node:crypto';
|
||||
import OAuth2Strategy from 'passport-oauth2';
|
||||
import { z } from 'zod';
|
||||
|
||||
/** @public */
|
||||
export interface OpenShiftAuthenticatorContext {
|
||||
openshiftApiServerUrl: string;
|
||||
helper: PassportOAuthAuthenticatorHelper;
|
||||
}
|
||||
|
||||
/** @private
|
||||
* Schema for user.openshift.io/v1,
|
||||
* see https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/user_and_group_apis/user-user-openshift-io-v1#user-user-openshift-io-v1
|
||||
*/
|
||||
const OpenShiftUser = z.object({
|
||||
metadata: z.object({
|
||||
name: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
/** @public */
|
||||
export const openshiftAuthenticator = createOAuthAuthenticator<
|
||||
OpenShiftAuthenticatorContext,
|
||||
PassportProfile
|
||||
>({
|
||||
defaultProfileTransform:
|
||||
PassportOAuthAuthenticatorHelper.defaultProfileTransform,
|
||||
scopes: {
|
||||
required: ['user:full'],
|
||||
},
|
||||
initialize({ callbackUrl, config }) {
|
||||
const clientId = config.getString('clientId');
|
||||
const clientSecret = config.getString('clientSecret');
|
||||
const authorizationUrl = config.getString('authorizationUrl');
|
||||
const tokenUrl = config.getString('tokenUrl');
|
||||
const openshiftApiServerUrl = config.getString('openshiftApiServerUrl');
|
||||
|
||||
// userUrl: `${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`,
|
||||
const strategy = new OAuth2Strategy(
|
||||
{
|
||||
clientID: clientId,
|
||||
clientSecret: clientSecret,
|
||||
callbackURL: callbackUrl,
|
||||
authorizationURL: authorizationUrl,
|
||||
tokenURL: tokenUrl,
|
||||
passReqToCallback: false,
|
||||
},
|
||||
(
|
||||
accessToken: any,
|
||||
refreshToken: string,
|
||||
params: any,
|
||||
fullProfile: PassportProfile,
|
||||
done: PassportOAuthDoneCallback,
|
||||
) => {
|
||||
done(undefined, { fullProfile, params, accessToken }, { refreshToken });
|
||||
},
|
||||
);
|
||||
|
||||
strategy.userProfile = function userProfile(
|
||||
accessToken: string,
|
||||
done: (err?: unknown, profile?: any) => void,
|
||||
): void {
|
||||
this._oauth2.useAuthorizationHeaderforGET(true);
|
||||
|
||||
this._oauth2.get(
|
||||
`${openshiftApiServerUrl}/apis/user.openshift.io/v1/users/~`,
|
||||
accessToken,
|
||||
(error, data, _) => {
|
||||
if (error !== null && error.statusCode !== 200) {
|
||||
done(new Error(`HTTP error! Status: ${error.statusCode}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
done(new Error('No data provided!'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof data !== 'string') {
|
||||
done(new Error('Data of type Buffer is not supported!'));
|
||||
return;
|
||||
}
|
||||
|
||||
const user = OpenShiftUser.parse(JSON.parse(data));
|
||||
done(null, { displayName: user.metadata.name });
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
openshiftApiServerUrl,
|
||||
helper: PassportOAuthAuthenticatorHelper.from(strategy),
|
||||
};
|
||||
},
|
||||
async start(input, { helper }) {
|
||||
return helper.start(input, {
|
||||
accessType: 'offline',
|
||||
prompt: 'consent',
|
||||
});
|
||||
},
|
||||
async authenticate(input, { helper }) {
|
||||
// Same workaround as the GitHub provider; see https://github.com/backstage/backstage/issues/25383
|
||||
const { fullProfile, session } = await helper.authenticate(input);
|
||||
session.refreshToken = session.accessToken;
|
||||
session.refreshTokenExpiresInSeconds = session.expiresInSeconds;
|
||||
return { fullProfile, session };
|
||||
},
|
||||
async refresh(input, { helper }) {
|
||||
// Because the session is refreshed on login, this override is crucial,
|
||||
// see https://github.com/backstage/backstage/issues/25383
|
||||
const accessToken = input.refreshToken;
|
||||
|
||||
const fullProfile = await helper.fetchProfile(accessToken).catch(error => {
|
||||
if (error.oauthError?.statusCode === 401) {
|
||||
throw new Error('Invalid access token');
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
return {
|
||||
fullProfile,
|
||||
session: {
|
||||
accessToken,
|
||||
tokenType: 'bearer',
|
||||
scope: input.scope,
|
||||
refreshToken: input.refreshToken,
|
||||
},
|
||||
};
|
||||
},
|
||||
async logout(input, { openshiftApiServerUrl, helper }) {
|
||||
// Due to the implementation of createOAuthRouteHandlers, only the refresh token is set.
|
||||
// In this provider, the refresh token actually IS the access token.
|
||||
const accessToken = input.refreshToken;
|
||||
if (!accessToken) {
|
||||
throw new Error('access token/refresh token needs to be set for logout');
|
||||
}
|
||||
|
||||
// Check if access token is still valid.
|
||||
try {
|
||||
await helper.fetchProfile(accessToken);
|
||||
} catch {
|
||||
// Invalid token, no need to delete OAuthAccessToken.
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate token name, see:
|
||||
// https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/oauth_apis/oauthaccesstoken-oauth-openshift-io-v1#apis-oauth-openshift-io-v1-oauthaccesstokens
|
||||
const tokenName = createHash('sha256')
|
||||
.update(accessToken.slice('sha256~'.length))
|
||||
.digest()
|
||||
.toString('base64url');
|
||||
|
||||
const response = await fetch(
|
||||
`${openshiftApiServerUrl}/apis/oauth.openshift.io/v1/oauthaccesstokens/sha256~${tokenName}`,
|
||||
{ method: 'DELETE', headers: { Authorization: `Bearer ${accessToken}` } },
|
||||
);
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new Error('unauthorized');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/**
|
||||
* The openshift-provider backend module for the auth plugin.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export {
|
||||
openshiftAuthenticator,
|
||||
type OpenShiftAuthenticatorContext,
|
||||
} from './authenticator';
|
||||
export { authModuleOpenshiftProvider as default } from './module';
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
authProvidersExtensionPoint,
|
||||
createOAuthProviderFactory,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
import { openshiftAuthenticator } from './authenticator';
|
||||
import { openshiftSignInResolvers } from './resolvers';
|
||||
|
||||
/** @public */
|
||||
export const authModuleOpenshiftProvider = createBackendModule({
|
||||
pluginId: 'auth',
|
||||
moduleId: 'openshift-provider',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { providers: authProvidersExtensionPoint },
|
||||
async init({ providers }) {
|
||||
providers.registerProvider({
|
||||
providerId: 'openshift',
|
||||
factory: createOAuthProviderFactory({
|
||||
authenticator: openshiftAuthenticator,
|
||||
signInResolverFactories: {
|
||||
...openshiftSignInResolvers,
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
createSignInResolverFactory,
|
||||
OAuthAuthenticatorResult,
|
||||
PassportProfile,
|
||||
SignInInfo,
|
||||
} from '@backstage/plugin-auth-node';
|
||||
|
||||
import {
|
||||
DEFAULT_NAMESPACE,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { z } from 'zod';
|
||||
|
||||
export namespace openshiftSignInResolvers {
|
||||
export const displayNameMatchingUserEntityName = createSignInResolverFactory({
|
||||
optionsSchema: z
|
||||
.object({
|
||||
dangerouslyAllowSignInWithoutUserInCatalog: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
create(options = {}) {
|
||||
return async (
|
||||
info: SignInInfo<OAuthAuthenticatorResult<PassportProfile>>,
|
||||
ctx,
|
||||
) => {
|
||||
const { displayName } = info.profile;
|
||||
|
||||
if (!displayName) {
|
||||
throw new Error(
|
||||
`OpenShift user profile does not contain a displayName`,
|
||||
);
|
||||
}
|
||||
|
||||
const userRef = stringifyEntityRef({
|
||||
kind: 'User',
|
||||
name: displayName,
|
||||
namespace: DEFAULT_NAMESPACE,
|
||||
});
|
||||
|
||||
return await ctx.signInWithCatalogUser(
|
||||
{ entityRef: userRef },
|
||||
{
|
||||
dangerousEntityRefFallback:
|
||||
options?.dangerouslyAllowSignInWithoutUserInCatalog
|
||||
? { entityRef: { name: displayName } }
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
bitbucketServerAuthApiRef,
|
||||
atlassianAuthApiRef,
|
||||
oneloginAuthApiRef,
|
||||
openshiftAuthApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { userSettingsTranslationRef } from '../../translation';
|
||||
import { useTranslationRef } from '@backstage/frontend-plugin-api';
|
||||
@@ -128,6 +129,14 @@ export const DefaultProviderSettings = (props: {
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
{configuredProviders.includes('openshift') && (
|
||||
<ProviderSettingsItem
|
||||
title="OpenShift"
|
||||
description="Provides authentication towards OpenShift APIs and identities"
|
||||
apiRef={openshiftAuthApiRef}
|
||||
icon={Star}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user