diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 355c8eb554..85cfc81d01 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -32,6 +32,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { + "-": "^0.0.1", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-client": "workspace:^", @@ -53,8 +54,12 @@ "@types/passport": "^1.0.3", "compression": "^1.7.4", "connect-session-knex": "^3.0.1", + "cookie": "^0.5.0", "cookie-parser": "^1.4.5", + "cookie-signature": "^1.2.1", "cors": "^2.8.5", + "d": "^1.0.1", + "e": "^0.2.32", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-session": "^1.17.1", @@ -81,6 +86,7 @@ "passport-onelogin-oauth": "^0.0.1", "passport-saml": "^3.1.2", "uuid": "^8.0.0", + "v": "^0.3.0", "winston": "^3.2.1", "yn": "^4.0.0" }, diff --git a/plugins/auth-backend/src/providers/pinniped/index.test.ts b/plugins/auth-backend/src/providers/pinniped/index.test.ts index 32749d3a1c..08d833fd34 100644 --- a/plugins/auth-backend/src/providers/pinniped/index.test.ts +++ b/plugins/auth-backend/src/providers/pinniped/index.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { pinniped } from '.'; +import { AuthProviderRouteHandlers } from '../types'; import { getVoidLogger } from '@backstage/backend-common'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; @@ -25,17 +26,101 @@ import request from 'supertest'; import cookieParser from 'cookie-parser'; import passport from 'passport'; import session from 'express-session'; +import signature from 'cookie-signature'; +import cookie from 'cookie'; describe('pinniped.create', () => { const server = setupServer(); setupRequestMockHandlers(server); + const nonce = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 16 bytes of zeros in base64 + const state = Buffer.from( + `nonce=${encodeURIComponent(nonce)}&env=development`, + ).toString('hex'); + + let app: express.Express; + let provider: AuthProviderRouteHandlers; + + beforeEach(() => { + server.use( + rest.all( + 'https://pinniped.test/.well-known/openid-configuration', + (_, res, ctx) => + res( + ctx.json({ + issuer: 'https://pinniped.test', + authorization_endpoint: 'https://pinniped.test/oauth2/authorize', + token_endpoint: 'https://pinniped.test/oauth2/token', + jwks_uri: 'https://pinniped.test/jwks.json', + response_types_supported: ['code'], + response_modes_supported: ['query', 'form_post'], + subject_types_supported: ['public'], + id_token_signing_alg_values_supported: ['ES256'], + token_endpoint_auth_methods_supported: ['client_secret_basic'], + scopes_supported: [ + 'openid', + 'offline_access', + 'pinniped:request-audience', + 'username', + 'groups', + ], + claims_supported: ['username', 'groups', 'additionalClaims'], + code_challenge_methods_supported: ['S256'], + 'discovery.supervisor.pinniped.dev/v1alpha1': { + pinniped_identity_providers_endpoint: + 'https://pinniped.test/v1alpha1/pinniped_identity_providers', + }, + }), + ), + ), + ); + provider = pinniped.create()({ + providerId: 'pinniped', + globalConfig: { + baseUrl: 'http://backstage.test/api/auth', + appUrl: 'http://backstage.test', + isOriginAllowed: _ => true, + }, + config: new ConfigReader({ + development: { + federationDomain: 'https://pinniped.test', + clientId: 'clientId', + clientSecret: 'clientSecret', + }, + }), + logger: getVoidLogger(), + resolverContext: { + issueToken: async _ => ({ token: '' }), + findCatalogUser: async _ => ({ + entity: { + apiVersion: '', + kind: '', + metadata: { name: '' }, + }, + }), + signInWithCatalogUser: async _ => ({ token: '' }), + }, + }); + const secret = 'secret'; + app = express() + .use(cookieParser(secret)) + .use( + session({ + secret, + saveUninitialized: false, + resave: false, + cookie: { secure: false }, + }), + ) + .use(passport.initialize()) + .use(passport.session()) + .use('/api/auth/pinniped/start', provider.start.bind(provider)) + .use( + '/api/auth/pinniped/handler/frame', + provider.frameHandler.bind(provider), + ); + }); describe('#start', () => { - const nonce = 'AAAAAAAAAAAAAAAAAAAAAA=='; // 16 bytes of zeros in base64 - const state = Buffer.from( - `nonce=${encodeURIComponent(nonce)}&env=development`, - ).toString('hex'); - const randomBytes = jest.spyOn( crypto, 'randomBytes', @@ -47,82 +132,7 @@ describe('pinniped.create', () => { it('redirects to authorization endpoint returned from federationDomain config value', async () => { randomBytes.mockReturnValue(Buffer.from(nonce, 'base64')); - server.use( - rest.all( - 'https://pinniped.test/.well-known/openid-configuration', - (_, res, ctx) => - res( - ctx.json({ - issuer: 'https://pinniped.test', - authorization_endpoint: - 'https://pinniped.test/oauth2/authorize', - token_endpoint: 'https://pinniped.test/oauth2/token', - jwks_uri: 'https://pinniped.test/jwks.json', - response_types_supported: ['code'], - response_modes_supported: ['query', 'form_post'], - subject_types_supported: ['public'], - id_token_signing_alg_values_supported: ['ES256'], - token_endpoint_auth_methods_supported: ['client_secret_basic'], - scopes_supported: [ - 'openid', - 'offline_access', - 'pinniped:request-audience', - 'username', - 'groups', - ], - claims_supported: ['username', 'groups', 'additionalClaims'], - code_challenge_methods_supported: ['S256'], - 'discovery.supervisor.pinniped.dev/v1alpha1': { - pinniped_identity_providers_endpoint: - 'https://pinniped.test/v1alpha1/pinniped_identity_providers', - }, - }), - ), - ), - ); - const provider = pinniped.create()({ - providerId: 'pinniped', - globalConfig: { - baseUrl: 'http://backstage.test/api/auth', - appUrl: 'http://backstage.test', - isOriginAllowed: _ => true, - }, - config: new ConfigReader({ - development: { - federationDomain: 'https://pinniped.test', - clientId: 'clientId', - clientSecret: 'clientSecret', - }, - }), - logger: getVoidLogger(), - resolverContext: { - issueToken: async _ => ({ token: '' }), - findCatalogUser: async _ => ({ - entity: { - apiVersion: '', - kind: '', - metadata: { name: '' }, - }, - }), - signInWithCatalogUser: async _ => ({ token: '' }), - }, - }); - - const secret = 'secret'; - const app = express() - .use(cookieParser(secret)) - .use( - session({ - secret, - saveUninitialized: false, - resave: false, - cookie: { secure: false }, - }), - ) - .use(passport.initialize()) - .use(passport.session()) - .use('/api/auth/pinniped/start', provider.start.bind(provider)); const responsePromise = request(app).get( '/api/auth/pinniped/start?' + 'env=development&scope=openid+pinniped:request-audience+username', @@ -146,4 +156,50 @@ describe('pinniped.create', () => { ); }); }); + describe('#frameHandler', () => { + it('performs an rfc 8693 token exchange after getting access token', async () => { + server.use( + rest.post('https://pinniped.test/oauth2/token', async (req, res, ctx) => + res( + ctx.json( + new URLSearchParams(await req.text()).get('grant_type') === + 'urn:ietf:params:oauth:grant-type:token-exchange' + ? { access_token: 'accessToken' } + : { id_token: 'clusterToken' }, + ), + ), + ), + ); + + const responsePromise = request(app) + .get( + '/api/auth/pinniped/handler/frame?' + + 'code=pin_ac_xU69qZGejOCu8Loz5iOD6Bm25SgQewmT0VVE1hOAQzA.WzxrI9bCder5UJHtCOX_yEnsM2OVh8pVSFI7NPs5yUM&' + + 'scope=openid+pinniped%3Arequest-audience+username&' + + `state=${state}`, + ) + .set( + 'Cookie', + `pinniped-nonce=${nonce}; ` + + 'connect.sid=s:p3_hKHiFr_i58jyTPIZxtWN9pejiOujD.SN2irLt6oIL18v0GzGCPO1sibEmzybiVlT9ca3ZjT68', + ); + const reqUrl = new URL(responsePromise.url); + reqUrl.search = ''; + server.use(rest.all(reqUrl.toString(), req => req.passthrough())); + + expect((await responsePromise).text).toContain( + encodeURIComponent( + JSON.stringify({ + type: 'authorization_response', + response: { + providerInfo: { + idToken: 'clusterToken', + }, + profile: {}, + }, + }), + ), + ); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index fd3b9da734..9f64dacf56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,13 @@ __metadata: version: 6 cacheKey: 8 +"-@npm:^0.0.1": + version: 0.0.1 + resolution: "-@npm:0.0.1" + checksum: 33786d96a8c404f3ce4db242b50d9a8f6013b3a0673bba52186b92f016429135ec2d9b9f77abf85c2a2b757c85e9b33a1f44d5dbf9740fd6294de87198681fd6 + languageName: node + linkType: hard + "@aashutoshrathi/word-wrap@npm:^1.2.3": version: 1.2.6 resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" @@ -4976,6 +4983,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend@workspace:plugins/auth-backend" dependencies: + "-": ^0.0.1 "@backstage/backend-common": "workspace:^" "@backstage/backend-defaults": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" @@ -5011,8 +5019,12 @@ __metadata: "@types/xml2js": ^0.4.7 compression: ^1.7.4 connect-session-knex: ^3.0.1 + cookie: ^0.5.0 cookie-parser: ^1.4.5 + cookie-signature: ^1.2.1 cors: ^2.8.5 + d: ^1.0.1 + e: ^0.2.32 express: ^4.17.1 express-promise-router: ^4.1.0 express-session: ^1.17.1 @@ -5041,6 +5053,7 @@ __metadata: passport-saml: ^3.1.2 supertest: ^6.1.3 uuid: ^8.0.0 + v: ^0.3.0 winston: ^3.2.1 yn: ^4.0.0 languageName: unknown @@ -20392,6 +20405,13 @@ __metadata: languageName: node linkType: hard +"async-limiter@npm:~1.0.0": + version: 1.0.1 + resolution: "async-limiter@npm:1.0.1" + checksum: 2b849695b465d93ad44c116220dee29a5aeb63adac16c1088983c339b0de57d76e82533e8e364a93a9f997f28bbfc6a92948cefc120652bd07f3b59f8d75cf2b + languageName: node + linkType: hard + "async-lock@npm:^1.1.0": version: 1.2.4 resolution: "async-lock@npm:1.2.4" @@ -22748,6 +22768,13 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:^1.2.1": + version: 1.2.1 + resolution: "cookie-signature@npm:1.2.1" + checksum: bb464aacac390b5d7d8ead2d6fff7c1c3b7378c7d0250921f48923fe889688e081ab33950448929db5f24d4f9f1506589a7ee1c685de8f12a3fdb30c49667ec5 + languageName: node + linkType: hard + "cookie@npm:0.4.1": version: 0.4.1 resolution: "cookie@npm:0.4.1" @@ -22762,7 +22789,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.5.0, cookie@npm:~0.5.0": +"cookie@npm:0.5.0, cookie@npm:^0.5.0, cookie@npm:~0.5.0": version: 0.5.0 resolution: "cookie@npm:0.5.0" checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180 @@ -23528,6 +23555,16 @@ __metadata: languageName: node linkType: hard +"d@npm:1, d@npm:^1.0.1": + version: 1.0.1 + resolution: "d@npm:1.0.1" + dependencies: + es5-ext: ^0.10.50 + type: ^1.0.1 + checksum: 49ca0639c7b822db670de93d4fbce44b4aa072cd848c76292c9978a8cd0fff1028763020ff4b0f147bd77bfe29b4c7f82e0f71ade76b2a06100543cdfd948d19 + languageName: node + linkType: hard + "dagre@npm:^0.8.5": version: 0.8.5 resolution: "dagre@npm:0.8.5" @@ -23613,6 +23650,16 @@ __metadata: languageName: node linkType: hard +"deasync@npm:^0.1.9": + version: 0.1.28 + resolution: "deasync@npm:0.1.28" + dependencies: + bindings: ^1.5.0 + node-addon-api: ^1.7.1 + checksum: e0c1ef427875c897e0d903a08410df1d0a3dfd0d2a0a1e43fb6c2824dfbc504b810bd08a0d30653117259316e1aa65409c96dbed40101c934f75bac7499e1265 + languageName: node + linkType: hard + "debounce@npm:^1.1.0, debounce@npm:^1.2.0": version: 1.2.1 resolution: "debounce@npm:1.2.1" @@ -23620,7 +23667,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2.6.9, debug@npm:^2.6.0": +"debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.1": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -23641,7 +23688,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^3.2.7": +"debug@npm:^3.1.0, debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" dependencies: @@ -24334,6 +24381,13 @@ __metadata: languageName: unknown linkType: soft +"e@npm:^0.2.32": + version: 0.2.32 + resolution: "e@npm:0.2.32" + checksum: 6fcebe65c37d44e69b03d1db3ea1a949855aaae227a3a7f80e807983d6f31f9dc4c9420c02ec6d37046ca0c5523fb836215c10e1cd9d8f438e6df701dc24de35 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -24738,6 +24792,17 @@ __metadata: languageName: node linkType: hard +"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.50": + version: 0.10.62 + resolution: "es5-ext@npm:0.10.62" + dependencies: + es6-iterator: ^2.0.3 + es6-symbol: ^3.1.3 + next-tick: ^1.1.0 + checksum: 25f42f6068cfc6e393cf670bc5bba249132c5f5ec2dd0ed6e200e6274aca2fed8e9aec8a31c76031744c78ca283c57f0b41c7e737804c6328c7b8d3fbcba7983 + languageName: node + linkType: hard + "es6-error@npm:^4.1.1": version: 4.1.1 resolution: "es6-error@npm:4.1.1" @@ -24745,6 +24810,17 @@ __metadata: languageName: node linkType: hard +"es6-iterator@npm:^2.0.3": + version: 2.0.3 + resolution: "es6-iterator@npm:2.0.3" + dependencies: + d: 1 + es5-ext: ^0.10.35 + es6-symbol: ^3.1.1 + checksum: 6e48b1c2d962c21dee604b3d9f0bc3889f11ed5a8b33689155a2065d20e3107e2a69cc63a71bd125aeee3a589182f8bbcb5c8a05b6a8f38fa4205671b6d09697 + languageName: node + linkType: hard + "es6-object-assign@npm:^1.1.0": version: 1.1.0 resolution: "es6-object-assign@npm:1.1.0" @@ -24752,6 +24828,16 @@ __metadata: languageName: node linkType: hard +"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3": + version: 3.1.3 + resolution: "es6-symbol@npm:3.1.3" + dependencies: + d: ^1.0.1 + ext: ^1.1.2 + checksum: cd49722c2a70f011eb02143ef1c8c70658d2660dead6641e160b94619f408b9cf66425515787ffe338affdf0285ad54f4eae30ea5bd510e33f8659ec53bcaa70 + languageName: node + linkType: hard + "esbuild-loader@npm:^2.18.0": version: 2.21.0 resolution: "esbuild-loader@npm:2.21.0" @@ -26073,6 +26159,15 @@ __metadata: languageName: node linkType: hard +"ext@npm:^1.1.2": + version: 1.7.0 + resolution: "ext@npm:1.7.0" + dependencies: + type: ^2.7.2 + checksum: ef481f9ef45434d8c867cfd09d0393b60945b7c8a1798bedc4514cb35aac342ccb8d8ecb66a513e6a2b4ec1e294a338e3124c49b29736f8e7c735721af352c31 + languageName: node + linkType: hard + "extend@npm:3.0.2, extend@npm:^3.0.0, extend@npm:^3.0.2, extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" @@ -33570,6 +33665,13 @@ __metadata: languageName: node linkType: hard +"next-tick@npm:^1.1.0": + version: 1.1.0 + resolution: "next-tick@npm:1.1.0" + checksum: 83b5cf36027a53ee6d8b7f9c0782f2ba87f4858d977342bfc3c20c21629290a2111f8374d13a81221179603ffc4364f38374b5655d17b6a8f8a8c77bdea4fe8b + languageName: node + linkType: hard + "nimma@npm:0.2.2": version: 0.2.2 resolution: "nimma@npm:0.2.2" @@ -33628,6 +33730,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^1.7.1": + version: 1.7.2 + resolution: "node-addon-api@npm:1.7.2" + dependencies: + node-gyp: latest + checksum: 938922b3d7cb34ee137c5ec39df6289a3965e8cab9061c6848863324c21a778a81ae3bc955554c56b6b86962f6ccab2043dd5fa3f33deab633636bd28039333f + languageName: node + linkType: hard + "node-addon-api@npm:^3.2.1": version: 3.2.1 resolution: "node-addon-api@npm:3.2.1" @@ -36634,7 +36745,7 @@ __metadata: languageName: node linkType: hard -"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": +"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.3, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" dependencies: @@ -39183,6 +39294,20 @@ __metadata: languageName: node linkType: hard +"simple-websocket@npm:^5.0.0": + version: 5.1.1 + resolution: "simple-websocket@npm:5.1.1" + dependencies: + debug: ^3.1.0 + inherits: ^2.0.1 + randombytes: ^2.0.3 + readable-stream: ^2.0.5 + safe-buffer: ^5.0.1 + ws: ^3.3.1 + checksum: 846ba5a4e8419a4b3186e8618ed55969d498d494c8c78002a7f6adfef51edfb1f673380ad28cd92d59c8a70d2247395ad8ad1117c1597839500e6c5efd1c3f30 + languageName: node + linkType: hard + "sinon@npm:^14.0.2": version: 14.0.2 resolution: "sinon@npm:14.0.2" @@ -41294,6 +41419,20 @@ __metadata: languageName: node linkType: hard +"type@npm:^1.0.1": + version: 1.2.0 + resolution: "type@npm:1.2.0" + checksum: dae8c64f82c648b985caf321e9dd6e8b7f4f2e2d4f846fc6fd2c8e9dc7769382d8a52369ddbaccd59aeeceb0df7f52fb339c465be5f2e543e81e810e413451ee + languageName: node + linkType: hard + +"type@npm:^2.7.2": + version: 2.7.2 + resolution: "type@npm:2.7.2" + checksum: 0f42379a8adb67fe529add238a3e3d16699d95b42d01adfe7b9a7c5da297f5c1ba93de39265ba30ffeb37dfd0afb3fb66ae09f58d6515da442219c086219f6f4 + languageName: node + linkType: hard + "typed-array-buffer@npm:^1.0.0": version: 1.0.0 resolution: "typed-array-buffer@npm:1.0.0" @@ -41481,6 +41620,13 @@ __metadata: languageName: node linkType: hard +"ultron@npm:~1.1.0": + version: 1.1.1 + resolution: "ultron@npm:1.1.1" + checksum: aa7b5ebb1b6e33287b9d873c6756c4b7aa6d1b23d7162ff25b0c0ce5c3c7e26e2ab141a5dc6e96c10ac4d00a372e682ce298d784f06ffcd520936590b4bc0653 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.0.2": version: 1.0.2 resolution: "unbox-primitive@npm:1.0.2" @@ -42050,6 +42196,20 @@ __metadata: languageName: node linkType: hard +"v@npm:^0.3.0": + version: 0.3.0 + resolution: "v@npm:0.3.0" + dependencies: + deasync: ^0.1.9 + debug: ^2.6.1 + simple-websocket: ^5.0.0 + dependenciesMeta: + deasync: + optional: true + checksum: 55a52287b7d417f348516d50e2103f3ef46db371e736da94d55ae2fdc61bdeb3f58eea689ffa69aebe2e93a1abd7a9424eabc552f6060884e434b872229b2045 + languageName: node + linkType: hard + "valid-url@npm:^1.0.9": version: 1.0.9 resolution: "valid-url@npm:1.0.9" @@ -42878,6 +43038,17 @@ __metadata: languageName: node linkType: hard +"ws@npm:^3.3.1": + version: 3.3.3 + resolution: "ws@npm:3.3.3" + dependencies: + async-limiter: ~1.0.0 + safe-buffer: ~5.1.0 + ultron: ~1.1.0 + checksum: 20b7bf34bb88715b9e2d435b76088d770e063641e7ee697b07543815fabdb752335261c507a973955e823229d0af8549f39cc669825e5c8404aa0422615c81d9 + languageName: node + linkType: hard + "ws@npm:^5.2.0 || ^6.0.0 || ^7.0.0, ws@npm:^7.4.6": version: 7.5.9 resolution: "ws@npm:7.5.9"