├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── .eslintignore ├── packages ├── ldap-auth │ ├── .npmignore │ ├── src │ │ ├── components │ │ │ ├── index.tsx │ │ │ └── LoginPage │ │ │ │ ├── style.tsx │ │ │ │ ├── types.ts │ │ │ │ ├── LoginPage.tsx │ │ │ │ ├── Form.tsx │ │ │ │ └── Identity.ts │ │ ├── index.ts │ │ ├── setupTests.ts │ │ ├── routes.ts │ │ ├── plugin.test.ts │ │ └── plugin.ts │ ├── tsconfig.json │ ├── dev │ │ └── index.tsx │ ├── app-config.yaml │ ├── package.json │ ├── README.md │ └── CHANGELOG.md └── ldap-auth-backend │ ├── .npmignore │ ├── tsconfig.json │ ├── src │ ├── index.ts │ ├── errors.ts │ ├── auth.ts │ ├── ldap.test.ts │ ├── jwt.ts │ ├── ldap.ts │ ├── alpha.ts │ ├── types.ts │ ├── alpha.test.ts │ ├── jwt.test.ts │ ├── provider.ts │ └── provider.test.ts │ ├── LICENSE │ ├── package.json │ ├── CHANGELOG.md │ └── README.md ├── .prettierignore ├── .czrc ├── .vscode └── settings.json ├── commitlint.config.js ├── screen.png ├── .prettierrc ├── lerna.json ├── .yarnrc.yml ├── .github ├── workflows │ ├── merge.yml │ ├── backmerge.yml │ ├── stale.yml │ ├── merge-dependabot.yml │ ├── release.yml │ └── test.yml ├── dependabot.yml └── pull_request_template.md ├── prepare.js ├── lint-staged.config.js ├── examples ├── custom-auth-options.ts ├── custom-check-user-exists.ts ├── enhance-user-object.md ├── jwt-token-store-postgres.md └── validate-api.md ├── .eslintrc ├── CONTRIBUTING.md ├── LICENSE ├── tsconfig.json ├── .versionrc.json ├── package.json ├── app-config.yaml ├── .gitignore ├── README.md └── CHANGELOG.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *dist* 2 | **/*.test.ts 3 | -------------------------------------------------------------------------------- /packages/ldap-auth/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/**/** 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | examples/** 2 | 3 | /.nx/workspace-data -------------------------------------------------------------------------------- /packages/ldap-auth-backend/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/**/** 3 | -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "./node_modules/cz-conventional-changelog" 3 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.errorSquiggles": "enabled" 3 | } 4 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/components/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './LoginPage/LoginPage'; 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/immobiliare/backstage-plugin-ldap-auth/HEAD/screen.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ldapAuthFrontendPlugin, LdapAuthFrontendPage } from './plugin'; 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit $1 5 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import 'cross-fetch/polyfill'; 3 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "npmClient": "yarn", 4 | "version": "4.3.1" 5 | } 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.3.1.cjs 8 | -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Run tests 8 | uses: ./.github/workflows/test.yml 9 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { createRouteRef } from '@backstage/core-plugin-api'; 2 | 3 | export const rootRouteRef = createRouteRef({ 4 | id: 'ldap-auth-frontend', 5 | }); 6 | -------------------------------------------------------------------------------- /prepare.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let isCi = false; 4 | 5 | try { 6 | isCi = require('is-ci'); 7 | } catch (_) { 8 | isCi = true; 9 | } 10 | 11 | if (!isCi) { 12 | require('husky').install(); 13 | } 14 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { ldapAuthFrontendPlugin } from './plugin'; 2 | 3 | describe('ldap-auth-frontend', () => { 4 | it('should export plugin', () => { 5 | expect(ldapAuthFrontendPlugin).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,css,json,md,yaml,yml}': ['prettier --write'], 3 | '**/**.md': (filenames) => 4 | filenames.map((filename) => `'markdown-toc -i ${filename}`), 5 | '*.ts': ['npm run style:lint', 'npm run style:prettier'], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ldap-auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@backstage/cli/config/tsconfig.json", 3 | "include": ["src", "dev"], 4 | "exclude": ["node_modules"], 5 | "compilerOptions": { 6 | "outDir": "dist-types", 7 | "rootDir": "." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@backstage/cli/config/tsconfig.json", 3 | "include": ["src", "dev", "migrations"], 4 | "exclude": ["node_modules"], 5 | "compilerOptions": { 6 | "outDir": "dist-types", 7 | "rootDir": "." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '@backstage/core-plugin-api'; 2 | 3 | import { LdapSignInPage } from './components/LoginPage/LoginPage'; 4 | 5 | export const ldapAuthFrontendPlugin = createPlugin({ 6 | id: 'ldap-auth-frontend', 7 | }); 8 | 9 | export const LdapAuthFrontendPage = LdapSignInPage; 10 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/index.ts: -------------------------------------------------------------------------------- 1 | export { prepareBackstageIdentityResponse } from './auth'; 2 | export { ldap, ProviderLdapAuthProvider } from './provider'; 3 | export { JWTTokenValidator, parseJwtPayload, normalizeTime } from './jwt'; 4 | export type { TokenValidator } from './jwt'; 5 | export * from './alpha'; 6 | export { default as default } from './alpha'; 7 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/errors.ts: -------------------------------------------------------------------------------- 1 | export const JWT_INVALID_TOKEN = 'JWT_INVALID_TOKEN'; 2 | export const JWT_EXPIRED_TOKEN = 'JWT_EXPIRED_TOKEN'; 3 | 4 | export const LDAP_CONNECT_FAIL = 'Failed to connect to LDAP server: '; 5 | 6 | export const AUTH_MISSING_CREDENTIALS = 7 | 'AUTH_MISSING_CREDENTIALS: provide username and password or token'; 8 | 9 | export const AUTH_USER_NOT_FOUND = 10 | 'AUTH_USER_NOT_FOUND: Credential invalid or user doesnt exists'; 11 | 12 | export const AUTH_USER_DATA_ERROR = 13 | 'AUTH_USER_DATA_ERROR: The user returned does not contain the configured usernameAttribute'; 14 | -------------------------------------------------------------------------------- /examples/custom-auth-options.ts: -------------------------------------------------------------------------------- 1 | export default async function createPlugin( 2 | env: PluginEnvironment, 3 | ): Promise { 4 | return await createRouter({ 5 | logger: env.logger, 6 | config: env.config, 7 | database: env.database, 8 | discovery: env.discovery, 9 | tokenManager: env.tokenManager, 10 | providerFactories: { 11 | ldap: ldap.create({ 12 | resolvers: { 13 | async ldapAuthentication(username, password, ldapOptions, authFunction): LDAPUser { 14 | const user = await authFunction(ldapOptions) 15 | return { uid: user.uid }; 16 | } 17 | } 18 | }, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/backmerge.yml: -------------------------------------------------------------------------------- 1 | name: Backmerge main -> next 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | backmerge: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false 16 | 17 | - name: Merge main -> next 18 | uses: devmasx/merge-branch@master 19 | with: 20 | type: now 21 | from_branch: main 22 | target_branch: next 23 | github_token: ${{ secrets.GH_NODE_TOKEN }} 24 | message: "Merge 'main' into 'next' [skip ci]" 25 | -------------------------------------------------------------------------------- /examples/custom-check-user-exists.ts: -------------------------------------------------------------------------------- 1 | export default async function createPlugin( 2 | env: PluginEnvironment, 3 | ): Promise { 4 | return await createRouter({ 5 | logger: env.logger, 6 | config: env.config, 7 | database: env.database, 8 | discovery: env.discovery, 9 | tokenManager: env.tokenManager, 10 | providerFactories: { 11 | ldap: ldap.create({ 12 | resolvers: { 13 | async checkUserExists(ldapAuthOptions, searchFunction): Promise { 14 | const { username } = ldapAuthOptions; 15 | 16 | // Do you custom checks 17 | // .... 18 | 19 | return true; 20 | } 21 | } 22 | }, 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v8 11 | with: 12 | days-before-issue-stale: 30 13 | days-before-issue-close: 5 14 | days-before-pr-stale: 45 15 | days-before-pr-close: 10 16 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 17 | stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | target-branch: next 11 | schedule: 12 | interval: 'daily' 13 | open-pull-requests-limit: 10 14 | 15 | - package-ecosystem: 'github-actions' 16 | directory: '/' 17 | target-branch: next 18 | schedule: 19 | interval: 'daily' 20 | open-pull-requests-limit: 10 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 🚨 Proposed changes 2 | 3 | > Please review the [guidelines for contributing](https://github.com/immobiliare/backstage-plugin-ldap-auth/CONTRIBUTING.md) to this repository. 4 | 5 | [[Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue.]] 6 | 7 | ## ⚙️ Types of changes 8 | 9 | What types of changes does your code introduce? _Put an `x` in the boxes that apply_ 10 | 11 | - [ ] New feature (non-breaking change which adds functionality) 12 | - [ ] Bugfix (non-breaking change which fixes an issue) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] Documentation Update (if none of the other choices apply) 15 | - [ ] Refactor 16 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/components/LoginPage/style.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import Grid from '@material-ui/core/Grid'; 4 | 5 | export type SignInPageClassKey = 'container' | 'item'; 6 | 7 | export const useStyles = makeStyles( 8 | { 9 | container: { 10 | padding: 0, 11 | listStyle: 'none', 12 | }, 13 | item: { 14 | display: 'flex', 15 | flexDirection: 'column', 16 | width: '100%', 17 | maxWidth: '400px', 18 | margin: 0, 19 | padding: 0, 20 | }, 21 | }, 22 | { name: 'BackstageSignInPage' } 23 | ); 24 | 25 | export const GridItem = ({ children }: { children: JSX.Element }) => { 26 | const classes = useStyles(); 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "ecmaVersion": 2022, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "es6": true, 9 | "node": true 10 | }, 11 | "extends": [ 12 | "prettier", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:prettier/recommended" 15 | ], 16 | "ignorePatterns": [ 17 | ".eslintrc.js", 18 | "**/dist/**", 19 | "**/dist-types/**", 20 | "**/examples/**" 21 | ], 22 | "rules": { 23 | "@typescript-eslint/no-var-requires": "off", 24 | "prefer-rest-params": "off", 25 | "@typescript-eslint/no-non-null-assertion": "off", 26 | "@typescript-eslint/ban-ts-comment": "warn", 27 | "@typescript-eslint/no-unused-vars": [ 28 | "error", 29 | { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/ldap-auth/dev/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevApp } from '@backstage/dev-utils'; 3 | import { LdapAuthFrontendPage, ldapAuthFrontendPlugin } from '../src'; 4 | import { IdentityApi } from '@backstage/core-plugin-api'; 5 | 6 | createDevApp() 7 | .registerPlugin(ldapAuthFrontendPlugin) 8 | .addPage({ 9 | element: ( 10 | 20 | ), 21 | title: 'Root Page', 22 | path: '/backstage-plugin-ldap', 23 | }) 24 | .render(); 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork the Project 4 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 5 | 3. Commit your Changes (`git commit -m 'feat(scope): some AmazingFeature'`) 6 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 7 | 5. Open a Pull Request 8 | 9 | ## Commit Convention 10 | 11 | This projects uses [commitlint](https://commitlint.js.org/) with Angular configuration so be sure to use standard commit format or PR won't be accepted 12 | 13 | ## Style 14 | 15 | Style and lint errors should be fixed with 16 | 17 | ```bash 18 | $ yarn style:lint && yarn style:prettier 19 | # follow instruction to fix if any 20 | ``` 21 | 22 | ## Supported Node.js versions 23 | 24 | All the current LTS versions are supported. 25 | 26 | ## Contributors 27 | 28 | [@JellyBellyDev](https://github.com/JellyBellyDev) 29 | [@simonecorsi](https://github.com/simonecorsi) 30 | [@dnlup](https://github.com/dnlup) 31 | [@antoniomuso](https://github.com/antoniomuso) 32 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/components/LoginPage/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BackstageIdentityResponse, 3 | ProfileInfo, 4 | } from '@backstage/core-plugin-api'; 5 | import { z } from 'zod'; 6 | 7 | export const ldapSessionSchema = z.object({ 8 | providerInfo: z.object({}).catchall(z.unknown()).optional(), 9 | profile: z.object({ 10 | email: z.string().optional(), 11 | displayName: z.string().optional(), 12 | picture: z.string().optional(), 13 | }), 14 | backstageIdentity: z.object({ 15 | token: z.string(), 16 | identity: z.object({ 17 | type: z.literal('user'), 18 | userEntityRef: z.string(), 19 | ownershipEntityRefs: z.array(z.string()), 20 | }), 21 | }), 22 | }); 23 | 24 | /** 25 | * Generic session information for proxied sign-in providers, e.g. common 26 | * reverse authenticating proxy implementations. 27 | * 28 | * @public 29 | */ 30 | export type LdapSession = { 31 | providerInfo?: { [key: string]: unknown }; 32 | profile: ProfileInfo; 33 | backstageIdentity: Omit; 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Immobiliare Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Immobiliare Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | 3 | on: pull_request 4 | 5 | permissions: 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Dependabot metadata 16 | id: metadata 17 | uses: dependabot/fetch-metadata@v1.6.0 18 | 19 | - name: Auto-merge patches 20 | if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' }} 21 | run: gh pr merge --auto --merge "$PR_URL" 22 | env: 23 | GH_TOKEN: ${{ secrets.DEPENDABOT_NODE_TOKEN }} 24 | PR_URL: ${{github.event.pull_request.html_url}} 25 | 26 | - name: Auto-merge dev-deps 27 | if: ${{ steps.dependabot-metadata.outputs.dependency-type == "direct:development" && steps.metadata.outputs.update-type == 'version-update:semver-minor' }} 28 | run: gh pr merge --auto --merge "$PR_URL" 29 | env: 30 | GH_TOKEN: ${{ secrets.DEPENDABOT_NODE_TOKEN }} 31 | PR_URL: ${{github.event.pull_request.html_url}} 32 | -------------------------------------------------------------------------------- /examples/enhance-user-object.md: -------------------------------------------------------------------------------- 1 | ```ts 2 | import { createRouter, ProfileInfo } from '@backstage/plugin-auth-backend'; 3 | import { Router } from 'express'; 4 | import { PluginEnvironment } from '../types'; 5 | 6 | import { ldap } from '@immobiliarelabs/backstage-plugin-ldap-auth-backend'; 7 | 8 | import crypto from 'crypto'; 9 | 10 | const md5 = crypto.createHash('md5'); 11 | 12 | export default async function createPlugin( 13 | env: PluginEnvironment, 14 | ): Promise { 15 | return await createRouter({ 16 | logger: env.logger, 17 | config: env.config, 18 | database: env.database, 19 | discovery: env.discovery, 20 | tokenManager: env.tokenManager, 21 | providerFactories: { 22 | ldap: ldap.create({ 23 | // this adds our Gitlab profile picture 24 | async authHandler({ uid }, ctx) { 25 | const backstageUserData = await ctx.findCatalogUser({ 26 | entityRef: uid as string, 27 | }); 28 | 29 | const profile = backstageUserData?.entity?.spec 30 | ?.profile as ProfileInfo; 31 | 32 | if (profile?.email) { 33 | const hash = md5.update(profile.email).digest('hex'); 34 | profile.picture = `https://www.gravatar.com/avatar/${hash}`; 35 | } 36 | 37 | return { 38 | profile, 39 | }; 40 | }, 41 | }), 42 | }, 43 | }); 44 | } 45 | 46 | ``` 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "examples"], 3 | "include": ["packages/*/src", "packages/*/dev"], 4 | "compilerOptions": { 5 | "outDir": "dist-types", 6 | "rootDir": ".", 7 | "useUnknownInCatchVariables": false, 8 | "allowJs": true, 9 | "declaration": true, 10 | "declarationMap": false, 11 | "esModuleInterop": true, 12 | "experimentalDecorators": false, 13 | "forceConsistentCasingInFileNames": true, 14 | "importHelpers": false, 15 | "incremental": true, 16 | "isolatedModules": true, 17 | "jsx": "react", 18 | "lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020"], 19 | "module": "ESNext", 20 | "moduleResolution": "node", 21 | "noFallthroughCasesInSwitch": true, 22 | "noImplicitAny": true, 23 | "noImplicitReturns": true, 24 | "noImplicitThis": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "pretty": true, 28 | "removeComments": false, 29 | "resolveJsonModule": true, 30 | "sourceMap": false, 31 | "skipLibCheck": true, 32 | "strict": true, 33 | "strictBindCallApply": true, 34 | "strictFunctionTypes": true, 35 | "strictNullChecks": true, 36 | "strictPropertyInitialization": true, 37 | "stripInternal": true, 38 | "target": "ES2019", 39 | "types": ["node", "jest", "webpack-env"], 40 | "useDefineForClassFields": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tagPrefix": "v", 3 | "name": "conventionalcommits", 4 | "owner": "immobiliare", 5 | "repository": "backstage-plugin-ldap-auth", 6 | "repoUrl": "https://github.com/immobiliare/backstage-plugin-ldap-auth", 7 | "types": [ 8 | { 9 | "type": "feat", 10 | "section": "Features" 11 | }, 12 | { 13 | "type": "fix", 14 | "section": "Bug Fixes" 15 | }, 16 | { 17 | "type": "ci", 18 | "section": "CI/CD" 19 | }, 20 | { 21 | "type": "refactor", 22 | "section": "Refactor" 23 | }, 24 | { 25 | "type": "perf", 26 | "section": "Performance" 27 | }, 28 | { 29 | "type": "docs", 30 | "section": "Documentation" 31 | }, 32 | { 33 | "type": "test", 34 | "section": "Test" 35 | }, 36 | { 37 | "type": "chore", 38 | "hidden": true 39 | }, 40 | { 41 | "type": "style", 42 | "hidden": true 43 | } 44 | ], 45 | "commitUrlFormat": "https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/{{hash}}", 46 | "compareUrlFormat": "https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/{{previousTag}}...{{currentTag}}", 47 | "issueUrlFormat": "https://github.com/immobiliare/backstage-plugin-ldap-auth/issues/{{id}}", 48 | "userUrlFormat": "https://github.com/{{user}}" 49 | } 50 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/auth.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | BackstageSignInResult, 3 | BackstageIdentityResponse, 4 | LDAPUser, 5 | } from './types'; 6 | 7 | import { ProfileInfo } from '@backstage/core-plugin-api'; 8 | import { AuthHandler } from '@backstage/plugin-auth-backend'; 9 | 10 | import { 11 | AuthResolverContext, 12 | SignInResolver, 13 | } from '@backstage/plugin-auth-node'; 14 | 15 | import { parseJwtPayload } from './jwt'; 16 | 17 | export function prepareBackstageIdentityResponse( 18 | result: BackstageSignInResult 19 | ): BackstageIdentityResponse { 20 | const { sub, ent } = parseJwtPayload(result.token); 21 | 22 | return { 23 | ...result, 24 | identity: { 25 | type: 'user', 26 | userEntityRef: sub, 27 | ownershipEntityRefs: ent || [], 28 | }, 29 | }; 30 | } 31 | 32 | export const defaultSigninResolver: SignInResolver = async ( 33 | { result }, 34 | ctx: AuthResolverContext 35 | ): Promise => { 36 | const backstageIdentity: BackstageSignInResult = 37 | await ctx.signInWithCatalogUser({ 38 | entityRef: result.uid as string, 39 | }); 40 | 41 | return backstageIdentity; 42 | }; 43 | 44 | export const defaultAuthHandler: AuthHandler = async ( 45 | { uid }, 46 | ctx: AuthResolverContext 47 | ): Promise<{ profile: ProfileInfo }> => { 48 | const backstageUserData = await ctx.findCatalogUser({ 49 | entityRef: uid as string, 50 | }); 51 | return { profile: backstageUserData?.entity?.spec?.profile as ProfileInfo }; 52 | }; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "devDependencies": { 8 | "@backstage/cli": "^0.26.11", 9 | "@commitlint/cli": "^17.1.2", 10 | "@commitlint/config-conventional": "^17.1.0", 11 | "@typescript-eslint/eslint-plugin": "^6.6.0", 12 | "@typescript-eslint/parser": "^6.1.0", 13 | "eslint": "^8.49.0", 14 | "eslint-config-prettier": "^9.0.0", 15 | "eslint-plugin-prettier": "^5.0.0", 16 | "husky": "^8.0.1", 17 | "is-ci": "^3.0.1", 18 | "lerna": "^8.1.7", 19 | "lint-staged": "^13.0.3", 20 | "markdown-toc": "^1.2.0", 21 | "prettier": "^3.0.3", 22 | "typescript": "^5.5.3" 23 | }, 24 | "engines": { 25 | "node": ">=18.0.0" 26 | }, 27 | "volta": { 28 | "node": "20.15.1" 29 | }, 30 | "scripts": { 31 | "style:lint": "eslint packages --ext .ts", 32 | "style:prettier": "prettier \"packages/**/*.ts\" --list-different --write", 33 | "build": "tsc && backstage-cli repo build --all", 34 | "test": "lerna run test:ci", 35 | "bootstrap": "npx lerna bootstrap", 36 | "publish:ci": "lerna publish from-package --yes --pre-dist-tag alpha", 37 | "version:release": "lerna version --yes --conventional-commits --conventional-graduate --create-release github --message 'chore(release): publish'", 38 | "version:prerelease": "lerna version --yes --force-publish --conventional-commits --conventional-prerelease --create-release github --message 'chore(prerelease): publish'", 39 | "prepare": "node prepare.js || echo 'Skipping prepare'" 40 | }, 41 | "packageManager": "yarn@4.3.1" 42 | } 43 | -------------------------------------------------------------------------------- /examples/jwt-token-store-postgres.md: -------------------------------------------------------------------------------- 1 | # Use a database to store JWT tokens for revalidation 2 | 3 | Since tokens are not stored in any way by Backstage we may want to do so to manually expire (eg: bans or deleted users). 4 | 5 | This examples uses Keyv with the same PostgreSQL db used internally by backstage, saving tokens in another table. 6 | 7 | ```ts 8 | import { createRouter, ProfileInfo } from '@backstage/plugin-auth-backend'; 9 | import { Router } from 'express'; 10 | import { PluginEnvironment } from '../types'; 11 | 12 | import { 13 | ldap, 14 | JWTTokenValidator, 15 | TokenValidator, 16 | } from '@immobiliarelabs/backstage-plugin-ldap-auth-backend'; 17 | import Keyv from 'keyv'; 18 | 19 | const CONNECTION_STRING = 'backend.database.connection'; 20 | 21 | export default async function createPlugin( 22 | env: PluginEnvironment 23 | ): Promise { 24 | const connection = env.config.getOptional(CONNECTION_STRING); 25 | 26 | const host = env.config.getOptionalString(`${CONNECTION_STRING}.host`); 27 | const port = env.config.getOptionalString(`${CONNECTION_STRING}.port`); 28 | const user = env.config.getOptionalString(`${CONNECTION_STRING}.user`); 29 | const password = env.config.getOptionalString( 30 | `${CONNECTION_STRING}.password` 31 | ); 32 | 33 | const tokenValidator: TokenValidator | undefined = new JWTTokenValidator( 34 | new Keyv( 35 | `postgresql://${user}:${password}@${host}:${port}/bs_jwt_tokens`, 36 | { table: 'token' } 37 | ) 38 | ); 39 | 40 | return await createRouter({ 41 | logger: env.logger, 42 | config: env.config, 43 | database: env.database, 44 | discovery: env.discovery, 45 | tokenManager: env.tokenManager, 46 | providerFactories: { 47 | ldap: ldap.create({ 48 | tokenValidator, 49 | }), 50 | }, 51 | }); 52 | } 53 | ``` 54 | 55 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/ldap.test.ts: -------------------------------------------------------------------------------- 1 | import { defaultCheckUserExists, defaultLDAPAuthentication } from './ldap'; 2 | import { AUTH_USER_DATA_ERROR } from './errors'; 3 | import { AuthenticationOptions } from 'ldap-authentication'; 4 | 5 | describe('LDAP authentication', () => { 6 | it('LDAP authentication success', async () => { 7 | const UID = 'ieqwiewqee'; 8 | const authFunc = jest.fn(() => 9 | Promise.resolve({ 10 | uid: UID, 11 | }) 12 | ); 13 | const out = await defaultLDAPAuthentication( 14 | 'john-doe', 15 | 'my-secure-password', 16 | { ldapOpts: { url: 'localhost' } }, 17 | authFunc 18 | ); 19 | 20 | expect(authFunc).toBeCalled(); 21 | expect(out).toEqual({ uid: UID }); 22 | }); 23 | 24 | it('LDAP authentication throws', async () => { 25 | const authFunc = jest.fn(() => 26 | Promise.resolve({ 27 | uid: '', 28 | }) 29 | ); 30 | const out = defaultLDAPAuthentication( 31 | 'john-doe', 32 | 'my-secure-password', 33 | { ldapOpts: { url: 'localhost' } }, 34 | authFunc 35 | ); 36 | 37 | expect(authFunc).toBeCalled(); 38 | expect(out).rejects.toEqual(new Error(AUTH_USER_DATA_ERROR)); 39 | }); 40 | }); 41 | 42 | describe('LDAP check user exists', () => { 43 | it('defaultCheckUserExists should forward to searchFunction if admin is defined', async () => { 44 | const options: AuthenticationOptions = { 45 | ldapOpts: { 46 | url: 'test.com', 47 | }, 48 | adminDn: 'admin', 49 | adminPassword: 'secretPassword', 50 | }; 51 | const authFunc = jest.fn(() => Promise.resolve(true)); 52 | await expect( 53 | defaultCheckUserExists(options, authFunc) 54 | ).resolves.toEqual(true); 55 | 56 | expect(authFunc).toBeCalledWith({ ...options, verifyUserExists: true }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /app-config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | title: Backstage Example App 3 | baseUrl: http://localhost:3000 4 | #datadogRum: 5 | # clientToken: '123456789' 6 | # applicationId: qwerty 7 | # site: # datadoghq.eu default = datadoghq.com 8 | # env: # optional 9 | support: 10 | url: https://github.com/backstage/backstage/issues # Used by common ErrorPage 11 | items: # Used by common SupportButton component 12 | - title: Issues 13 | icon: github 14 | links: 15 | - url: https://github.com/backstage/backstage/issues 16 | title: GitHub Issues 17 | - title: Discord Chatroom 18 | icon: chat 19 | links: 20 | - url: https://discord.gg/MUpMjP2 21 | title: '#backstage' 22 | 23 | backend: 24 | # Used for enabling authentication, secret is shared by all backend plugins 25 | # See https://backstage.io/docs/tutorials/backend-to-backend-auth for 26 | # information on the format 27 | # auth: 28 | # keys: 29 | # - secret: ${BACKEND_SECRET} 30 | baseUrl: http://localhost:7007 31 | listen: 32 | port: 7007 33 | database: 34 | client: better-sqlite3 35 | connection: ':memory:' 36 | cache: 37 | store: memory 38 | cors: 39 | origin: http://localhost:3000 40 | methods: [GET, HEAD, PATCH, POST, PUT, DELETE] 41 | credentials: true 42 | csp: 43 | connect-src: ["'self'", 'http:', 'https:'] 44 | # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference 45 | # Default Helmet Content-Security-Policy values can be removed by setting the key to false 46 | reading: 47 | allow: 48 | - host: example.com 49 | - host: '*.mozilla.org' 50 | - host: gitlab.com 51 | # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir 52 | 53 | integrations: 54 | gitlab: 55 | - host: gitlab.com 56 | -------------------------------------------------------------------------------- /packages/ldap-auth/app-config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | title: Backstage Example App 3 | baseUrl: http://localhost:3000 4 | #datadogRum: 5 | # clientToken: '123456789' 6 | # applicationId: qwerty 7 | # site: # datadoghq.eu default = datadoghq.com 8 | # env: # optional 9 | support: 10 | url: https://github.com/backstage/backstage/issues # Used by common ErrorPage 11 | items: # Used by common SupportButton component 12 | - title: Issues 13 | icon: github 14 | links: 15 | - url: https://github.com/backstage/backstage/issues 16 | title: GitHub Issues 17 | - title: Discord Chatroom 18 | icon: chat 19 | links: 20 | - url: https://discord.gg/MUpMjP2 21 | title: '#backstage' 22 | 23 | backend: 24 | # Used for enabling authentication, secret is shared by all backend plugins 25 | # See https://backstage.io/docs/tutorials/backend-to-backend-auth for 26 | # information on the format 27 | # auth: 28 | # keys: 29 | # - secret: ${BACKEND_SECRET} 30 | baseUrl: http://localhost:7007 31 | listen: 32 | port: 7007 33 | database: 34 | client: better-sqlite3 35 | connection: ':memory:' 36 | cache: 37 | store: memory 38 | cors: 39 | origin: http://localhost:3000 40 | methods: [GET, HEAD, PATCH, POST, PUT, DELETE] 41 | credentials: true 42 | csp: 43 | connect-src: ["'self'", 'http:', 'https:'] 44 | # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference 45 | # Default Helmet Content-Security-Policy values can be removed by setting the key to false 46 | reading: 47 | allow: 48 | - host: example.com 49 | - host: '*.mozilla.org' 50 | - host: gitlab.com 51 | # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir 52 | 53 | integrations: 54 | gitlab: 55 | - host: gitlab.com 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | release: 7 | name: Release 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | persist-credentials: false 13 | fetch-depth: 0 14 | 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 20 18 | # setting this should create the npmrc with $NODE_AUTH_TOKEN 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Config git user 22 | run: | 23 | git config --global user.name "${{ github.actor }}" 24 | git config --global user.email "${{ github.actor }}@users.noreply.github.com" 25 | git remote set-url origin https://${{ github.actor }}:${{ secrets.GH_NODE_TOKEN }}@github.com/${{ github.repository }} 26 | 27 | - name: Bootstrap lerna 28 | run: yarn 29 | 30 | - name: Build packages 31 | run: yarn build 32 | 33 | - name: Bump release 34 | if: github.ref == 'refs/heads/main' 35 | run: yarn version:release 36 | env: 37 | GH_TOKEN: ${{ secrets.GH_NODE_TOKEN }} 38 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 39 | 40 | - name: Bump prerelease 41 | if: github.ref != 'refs/heads/main' 42 | run: yarn version:prerelease 43 | env: 44 | GH_TOKEN: ${{ secrets.GH_NODE_TOKEN }} 45 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 46 | 47 | - name: Publish packages 48 | run: yarn publish:ci 49 | env: 50 | GH_TOKEN: ${{ secrets.GH_NODE_TOKEN }} 51 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 52 | 53 | - name: merge main -> next 54 | uses: actions/setup-node@v3 55 | with: 56 | type: now 57 | from_branch: main 58 | target_branch: next 59 | github_token: ${{ secrets.GH_NODE_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [workflow_call] 4 | 5 | jobs: 6 | conventional-commit-checker: 7 | name: Conventional Commit Checker 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | token: ${{ secrets.GITHUB_TOKEN }} 13 | - uses: webiny/action-conventional-commits@v1.1.0 14 | type-checker: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 20.x 23 | 24 | - name: Install dependencies 25 | run: yarn install --immutable 26 | 27 | - name: Test 28 | run: yarn tsc --noEmit 29 | fmt-checker: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | token: ${{ secrets.GITHUB_TOKEN }} 35 | - uses: actions/setup-node@v3 36 | with: 37 | node-version: 20.x 38 | - name: Install dependencies 39 | run: yarn install 40 | - name: Test 41 | run: yarn prettier --check . 42 | build: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | with: 47 | token: ${{ secrets.GITHUB_TOKEN }} 48 | - uses: actions/setup-node@v3 49 | with: 50 | node-version: 20.x 51 | - name: Install dependencies 52 | run: yarn install 53 | - name: Build packages 54 | run: yarn build 55 | test: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v4 59 | with: 60 | token: ${{ secrets.GITHUB_TOKEN }} 61 | - uses: actions/setup-node@v3 62 | with: 63 | node-version: 20.x 64 | 65 | - name: Install dependencies 66 | run: yarn 67 | 68 | - name: Test 69 | run: yarn test 70 | -------------------------------------------------------------------------------- /packages/ldap-auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@immobiliarelabs/backstage-plugin-ldap-auth", 3 | "description": "Backstage LDAP Authentication plugin, this packages adds frontend login page and token management sibling of @immobiliarelabs/backstage-plugin-ldap-auth-backend", 4 | "version": "4.3.1", 5 | "main": "dist/index.esm.js", 6 | "types": "dist/index.d.ts", 7 | "license": "MIT", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "backstage": { 12 | "role": "frontend-plugin", 13 | "pluginId": "ldap-auth", 14 | "pluginPackages": [ 15 | "@immobiliarelabs/backstage-plugin-ldap-auth" 16 | ] 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git@github.com:immobiliare/backstage-plugin-ldap-auth.git", 21 | "directory": "packages/ldap-auth" 22 | }, 23 | "sideEffects": false, 24 | "scripts": { 25 | "start": "backstage-cli package start", 26 | "types": "tsc", 27 | "build": "backstage-cli package build", 28 | "lint": "backstage-cli package lint", 29 | "test": "backstage-cli package test", 30 | "test:ci": "backstage-cli package test --watch false", 31 | "clean": "backstage-cli package clean", 32 | "prepack": "backstage-cli package prepack", 33 | "postpack": "backstage-cli package postpack" 34 | }, 35 | "dependencies": { 36 | "@backstage/core-components": "^0.14.9", 37 | "@backstage/core-plugin-api": "^1.9.3", 38 | "@backstage/theme": "^0.5.6", 39 | "@material-ui/core": "^4.12.4", 40 | "@react-hookz/web": "^23.0.0", 41 | "password-validator": "^5.3.0", 42 | "zod": "^3.17.3" 43 | }, 44 | "peerDependencies": { 45 | "react": "^16.13.1 || ^17.0.0" 46 | }, 47 | "devDependencies": { 48 | "@backstage/cli": "^0.26.11", 49 | "@backstage/dev-utils": "^1.0.35", 50 | "@testing-library/jest-dom": "^5.10.1", 51 | "@types/jest": "^29.0.3", 52 | "@types/node": "^20.5.9", 53 | "cross-fetch": "^3.1.5", 54 | "react": "^18.2.0", 55 | "react-dom": "^18.2.0", 56 | "react-router": "^6.4.5", 57 | "react-router-dom": "^6.4.5" 58 | }, 59 | "files": [ 60 | "dist" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@immobiliarelabs/backstage-plugin-ldap-auth-backend", 3 | "description": "Backstage LDAP Authentication plugin, this packages adds backend authentication and token generation/validation/management; sibling of @immobiliarelabs/backstage-plugin-ldap-auth", 4 | "version": "4.3.1", 5 | "main": "dist/index.cjs.js", 6 | "types": "dist/index.d.ts", 7 | "license": "MIT", 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git@github.com:immobiliare/backstage-plugin-ldap-auth.git", 14 | "directory": "packages/ldap-auth-backend" 15 | }, 16 | "backstage": { 17 | "role": "backend-plugin-module", 18 | "pluginId": "auth", 19 | "pluginPackage": "@backstage/plugin-auth-backend" 20 | }, 21 | "scripts": { 22 | "start": "backstage-cli package start", 23 | "types": "tsc", 24 | "build": "backstage-cli package build", 25 | "lint": "backstage-cli package lint", 26 | "test": "backstage-cli package test", 27 | "test:ci": "backstage-cli package test --watch false", 28 | "clean": "backstage-cli package clean", 29 | "prepack": "backstage-cli package prepack", 30 | "postpack": "backstage-cli package postpack" 31 | }, 32 | "dependencies": { 33 | "@backstage/backend-plugin-api": "^0.7.0", 34 | "@backstage/core-plugin-api": "^1.9.3", 35 | "@backstage/errors": "^1.2.4", 36 | "@backstage/plugin-auth-backend": "^0.22.9", 37 | "@backstage/plugin-auth-node": "^0.4.17", 38 | "keyv": "^4.3.3", 39 | "ldap-authentication": "^3.2.2", 40 | "ldap-escape": "^2.0.6", 41 | "ldapjs": "^3.0.7" 42 | }, 43 | "peerDependencies": { 44 | "react": "^16.13.1 || ^17.0.0" 45 | }, 46 | "devDependencies": { 47 | "@aws-sdk/middleware-sdk-sts": "^3.616.0", 48 | "@backstage/backend-test-utils": "^0.4.4", 49 | "@backstage/cli": "^0.26.11", 50 | "@types/ldap-escape": "^2.0.2", 51 | "@types/ldapjs": "^2.2.4", 52 | "@types/node": "^20.5.9", 53 | "@types/supertest": "^6.0.2", 54 | "supertest": "^6.3.4", 55 | "typescript": "^5.5.3" 56 | }, 57 | "files": [ 58 | "dist" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | packages/**/dist-types/ 2 | dist-types 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/jwt.ts: -------------------------------------------------------------------------------- 1 | import type Keyv from 'keyv'; 2 | 3 | import type { BackstageJWTPayload } from './types'; 4 | import { JWT_INVALID_TOKEN, JWT_EXPIRED_TOKEN } from './errors'; 5 | 6 | export const COOKIE_FIELD_KEY = 'backstage-token'; 7 | 8 | export const normalizeTime = (date: number) => Math.floor(date / 1000); 9 | 10 | export function parseJwtPayload(token: string): BackstageJWTPayload | never { 11 | try { 12 | // header, payload, signature 13 | const [, payload] = token.split('.'); 14 | return JSON.parse(Buffer.from(payload, 'base64').toString()); 15 | } catch (e) { 16 | throw new Error(JWT_INVALID_TOKEN); 17 | } 18 | } 19 | 20 | export interface TokenValidator { 21 | logout(jwt: string, ts: number): Promise | void; 22 | isValid(jwt: string): Promise | boolean; 23 | invalidateToken(jwt: string): Promise | void; 24 | } 25 | 26 | export class JWTTokenValidator implements TokenValidator { 27 | protected readonly store: Keyv; 28 | readonly increaseTokenExpireMs: number; 29 | 30 | constructor(store: Keyv, increaseTokenExpireMs?: number) { 31 | this.store = store; 32 | this.increaseTokenExpireMs = isNaN(increaseTokenExpireMs || 0) 33 | ? 0 34 | : increaseTokenExpireMs || 0; 35 | } 36 | 37 | async logout(jwt: string, ts: number): Promise { 38 | await this.isValid(jwt); 39 | const { sub } = parseJwtPayload(jwt); 40 | await this.store.set(sub, ts); 41 | } 42 | 43 | // On logout and refresh set the new invalidBeforeDate for the user 44 | async invalidateToken(jwt: string) { 45 | await this.isValid(jwt); 46 | const { sub } = parseJwtPayload(jwt); 47 | await this.store.set(sub, normalizeTime(Date.now())); 48 | } 49 | 50 | // rejects tokens issued before logouts and refresh 51 | async isValid(jwt: string) { 52 | const { sub, iat, exp } = parseJwtPayload(jwt); 53 | 54 | if ( 55 | normalizeTime(Date.now()) > 56 | exp + normalizeTime(this.increaseTokenExpireMs) 57 | ) { 58 | // is expired? 59 | throw new Error(JWT_EXPIRED_TOKEN); 60 | } 61 | 62 | // check if we have and entry in the cache 63 | if (await this.store.has(sub)) { 64 | const invalidBeforeDate = await this.store.get(sub); 65 | 66 | // if user signed off 67 | if (invalidBeforeDate && iat < invalidBeforeDate) { 68 | throw new Error(JWT_EXPIRED_TOKEN); 69 | } 70 | } 71 | 72 | return true; 73 | } 74 | } 75 | 76 | export class TokenValidatorNoop implements TokenValidator { 77 | // On logout and refresh set the new invalidBeforeDate for the user 78 | async invalidateToken(_jwt: string) { 79 | return; 80 | } 81 | 82 | async logout(_jwt: string, _ts: number) { 83 | return; 84 | } 85 | // rejects tokens issued before logouts and refreshs 86 | async isValid(_jwt: string) { 87 | return true; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

Backstage Plugin LDAP Auth

5 | 6 | ![release workflow](https://img.shields.io/github/workflow/status/immobiliare/backstage-plugin-ldap-auth/Release?style=flat-square) 7 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier?style=flat-square) 8 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release) 9 | ![license](https://img.shields.io/github/license/immobiliare/backstage-plugin-ldap-auth?style=flat-square) 10 | ![npm (scoped)](https://img.shields.io/npm/v/@immobiliarelabs/backstage-plugin-ldap-auth?style=flat-square) 11 | 12 | > [Backstage](https://backstage.io/) plugins to authenticate users to an LDAP server 13 | 14 | This is a monorepo containing two Backstage plugins, one for the frontend and one for the backend, both used to provide login screen, token management and server-side auth logic to your LDAP server. 15 | 16 | This project is also meant to be used in pair with the official [@backstage/plugin-catalog-backend-module-ldap](https://www.npmjs.com/package/@backstage/plugin-catalog-backend-module-ldap) which imports and keeps in sync your LDAP users but won't authenticate them. 17 | 18 | ## Plugins 19 | 20 | > Check the corrisponding README for both packages 21 | 22 | - [`packages/ldap-auth-backend`](./packages/ldap-auth-backend/README.md) - Back to back authentication and token validation and management 23 | - [`packages/ldap-auth`](./packages/ldap-auth/README.md)- Frontend Login Page and token usage and retention logics 24 | 25 |

26 | 27 |

28 | 29 | ## Backstage compatibility 30 | 31 | | backstage | | 32 | | --------- | ------------ | 33 | | 1.6.x | `stable` | 34 | | <=1.5.x | `not tested` | 35 | 36 | ## Powered Apps 37 | 38 | Backstage Plugin LDAP Auth was created by the amazing Node.js team at [ImmobiliareLabs](http://labs.immobiliare.it/), the Tech dept of [Immobiliare.it](https://www.immobiliare.it), the #1 real estate company in Italy. 39 | 40 | We are currently using Backstage Plugin LDAP Auth in our products as well as our internal toolings. 41 | 42 | **If you are using Backstage Plugin LDAP Auth in production [drop us a message](mailto:opensource@immobiliare.it)**. 43 | 44 | ## Support & Contribute 45 | 46 | Made with ❤️ by [ImmobiliareLabs](https://github.com/immobiliare) & [Contributors](https://github.com/immobiliare/backstage-plugin-ldap-auth/CONTRIBUTING.md#contributors) 47 | 48 | We'd love for you to contribute to Backstage Plugin LDAP Auth! 49 | If you have any questions on how to use Backstage Plugin LDAP Auth, bugs and enhancement please feel free to reach out by opening a [GitHub Issue](https://github.com/immobiliare/backstage-plugin-ldap-auth). 50 | 51 | ## License 52 | 53 | Backstage Plugin LDAP Auth is licensed under the MIT license. 54 | See the [LICENSE](https://github.com/immobiliare/backstage-plugin-ldap-auth/LICENSE) file for more information. 55 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/ldap.ts: -------------------------------------------------------------------------------- 1 | import type { LDAPUser } from './types'; 2 | 3 | import ldap from 'ldapjs'; 4 | import { dn } from 'ldap-escape'; 5 | import { authenticate, AuthenticationOptions } from 'ldap-authentication'; 6 | 7 | import { 8 | AUTH_USER_DATA_ERROR, 9 | AUTH_USER_NOT_FOUND, 10 | LDAP_CONNECT_FAIL, 11 | } from './errors'; 12 | 13 | async function _verifyUserExistsNoAdmin( 14 | searchString: string, 15 | ldapOpts: ldap.ClientOptions, 16 | searchOpts = {} 17 | ): Promise { 18 | const client: ldap.Client = await new Promise((resolve, reject) => { 19 | ldapOpts.connectTimeout = ldapOpts.connectTimeout || 5000; 20 | const client = ldap.createClient(ldapOpts); 21 | 22 | client.on('connect', () => resolve(client)); 23 | client.on('timeout', reject); 24 | client.on('connectTimeout', reject); 25 | client.on('error', reject); 26 | client.on('connectError', reject); 27 | }); 28 | if (client instanceof Error) 29 | throw new Error(`${LDAP_CONNECT_FAIL} ${client.message}`); 30 | return new Promise((resolve, reject) => { 31 | client.search(searchString, searchOpts, (error, res) => { 32 | if (error) reject(error); 33 | let exists = false; // avoids double resolve call if user exists 34 | res.on('searchEntry', () => (exists = true)); 35 | res.on('error', () => client.unbind()); 36 | res.on('end', () => { 37 | resolve(exists); 38 | client.unbind(); 39 | }); 40 | }); 41 | }); 42 | } 43 | 44 | export const defaultCheckUserExists = async ( 45 | ldapAuthOptions: AuthenticationOptions, 46 | searchFunction: typeof authenticate 47 | ): Promise => { 48 | // This fallback is for clients with no need for an admin to list users 49 | // I'll remove this if we can add this option to the auth library. 50 | if (!ldapAuthOptions.adminDn || !ldapAuthOptions.adminPassword) { 51 | const { 52 | username, 53 | userSearchBase, 54 | usernameAttribute = 'uid', 55 | } = ldapAuthOptions; 56 | return _verifyUserExistsNoAdmin( 57 | dn`${usernameAttribute as string}=${username as string},` + 58 | userSearchBase, 59 | ldapAuthOptions.ldapOpts 60 | ); 61 | } 62 | 63 | return searchFunction({ 64 | ...ldapAuthOptions, 65 | // this is used to serach for the user 66 | verifyUserExists: true, 67 | }); 68 | }; 69 | 70 | export async function defaultLDAPAuthentication( 71 | username: string, 72 | password: string, 73 | ldapAuthOptions: AuthenticationOptions, 74 | authFunction: typeof authenticate 75 | ): Promise { 76 | const { usernameAttribute = 'uid' } = ldapAuthOptions; 77 | 78 | const userDn = 79 | dn`${usernameAttribute as string}=${username as string},` + 80 | ldapAuthOptions.userSearchBase; 81 | 82 | const authObj = { 83 | ...ldapAuthOptions, 84 | username, 85 | usernameAttribute, 86 | userDn, 87 | userPassword: password, 88 | }; 89 | 90 | try { 91 | const user = await authFunction(authObj); 92 | if (!user) { 93 | throw new Error(AUTH_USER_NOT_FOUND); 94 | } 95 | if (!user[usernameAttribute as string]) { 96 | throw new Error(AUTH_USER_DATA_ERROR); 97 | } 98 | return { uid: user[usernameAttribute as string] }; 99 | } catch (e) { 100 | console.error( 101 | 'There was an error when trying to login with ldap-authentication' 102 | ); 103 | throw e; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/alpha.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RootConfigService, 3 | coreServices, 4 | createBackendModule, 5 | createExtensionPoint, 6 | createServiceFactory, 7 | createServiceRef, 8 | } from '@backstage/backend-plugin-api'; 9 | import { authProvidersExtensionPoint } from '@backstage/plugin-auth-node'; 10 | import { JWTTokenValidator, TokenValidator } from './jwt'; 11 | import { ldap } from './provider'; 12 | import { 13 | LDAPResponse, 14 | ProviderCreateOptions, 15 | Resolvers, 16 | SignInResolver, 17 | } from './types'; 18 | import { AuthHandler } from '@backstage/plugin-auth-backend'; 19 | import Keyv from 'keyv'; 20 | 21 | interface LdapAuthSetter { 22 | set(opt: ProviderCreateOptions): void; 23 | } 24 | 25 | class LdapAuthExt implements LdapAuthSetter { 26 | #authHandler: AuthHandler> | undefined; 27 | #resolvers: Resolvers | undefined; 28 | #signInResolver: SignInResolver | undefined; 29 | #tokenValidatorExt: TokenValidator | undefined; 30 | set(opt: ProviderCreateOptions): void { 31 | this.#authHandler = opt.authHandler; 32 | this.#resolvers = opt.resolvers; 33 | this.#signInResolver = opt.signIn; 34 | this.#tokenValidatorExt = opt.tokenValidator; 35 | } 36 | 37 | get authHandler() { 38 | return this.#authHandler; 39 | } 40 | get resolvers() { 41 | return this.#resolvers; 42 | } 43 | get signInResolver() { 44 | return this.#signInResolver; 45 | } 46 | get tokenValidatorExt() { 47 | return this.#tokenValidatorExt; 48 | } 49 | } 50 | export const ldapAuthExtensionPoint = createExtensionPoint({ 51 | id: 'ldap-auth-extension', 52 | }); 53 | 54 | export const tokenValidatorRef = createServiceRef({ 55 | scope: 'plugin', 56 | id: 'token-validator', 57 | defaultFactory: async (service) => 58 | createServiceFactory({ 59 | service, 60 | deps: { config: coreServices.rootConfig }, 61 | factory({}) { 62 | return new JWTTokenValidator(new Keyv()); 63 | }, 64 | }), 65 | }); 66 | 67 | type TokenValidatorOptions = { 68 | createTokenValidator( 69 | config: RootConfigService 70 | ): TokenValidator | Promise; 71 | }; 72 | 73 | export const tokenValidatorFactoryWithOptions = ( 74 | options?: TokenValidatorOptions 75 | ) => 76 | createServiceFactory({ 77 | service: tokenValidatorRef, 78 | deps: { config: coreServices.rootConfig }, 79 | factory({ config }) { 80 | return ( 81 | options?.createTokenValidator(config) || 82 | new JWTTokenValidator(new Keyv()) 83 | ); 84 | }, 85 | }); 86 | 87 | export const tokenValidatorFactory = Object.assign( 88 | tokenValidatorFactoryWithOptions, 89 | tokenValidatorFactoryWithOptions() 90 | ); 91 | 92 | export default createBackendModule({ 93 | pluginId: 'auth', 94 | moduleId: 'ldap', 95 | register(reg) { 96 | const ldapAuthSetter = new LdapAuthExt(); 97 | reg.registerExtensionPoint( 98 | ldapAuthExtensionPoint, 99 | ldapAuthSetter 100 | ); 101 | 102 | reg.registerInit({ 103 | deps: { 104 | providers: authProvidersExtensionPoint, 105 | tokenValidator: tokenValidatorRef, 106 | }, 107 | async init({ providers, tokenValidator }) { 108 | providers.registerProvider({ 109 | providerId: 'ldap', 110 | factory: ldap.create({ 111 | tokenValidator: 112 | ldapAuthSetter.tokenValidatorExt || tokenValidator, 113 | authHandler: ldapAuthSetter.authHandler, 114 | resolvers: ldapAuthSetter.resolvers, 115 | signIn: ldapAuthSetter.signInResolver, 116 | }), 117 | }); 118 | }, 119 | }); 120 | }, 121 | }); 122 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/components/LoginPage/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Backstage Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | discoveryApiRef, 19 | SignInPageProps, 20 | useApi, 21 | } from '@backstage/core-plugin-api'; 22 | import React, { useState, useEffect } from 'react'; 23 | import { useAsync } from '@react-hookz/web'; 24 | import { Progress } from '@backstage/core-components'; 25 | import { LdapSignInIdentity } from './Identity'; 26 | import { LoginForm } from './Form'; 27 | 28 | /** 29 | * Props for {@link LdapSignInPage}. 30 | * 31 | * @public 32 | */ 33 | export type LdapSignInPageProps = SignInPageProps & { 34 | provider: string; 35 | children?: React.ReactNode | null; 36 | onSignInError?: (error: Error) => void; 37 | options?: { 38 | helperTextPassword?: string; 39 | helperTextUsername?: string; 40 | validateUsername?: (usr: string) => boolean; 41 | validatePassword?: (pass: string) => boolean; 42 | usernameLabel?: string; 43 | }; 44 | }; 45 | 46 | /** 47 | * A sign-in page that has no user interface of its own. Instead, it relies on 48 | * sign-in being performed by a reverse authenticating proxy that Backstage is 49 | * deployed behind, and leverages its session handling. 50 | * 51 | * @remarks 52 | * 53 | * This sign-in page is useful when you are using products such as Google 54 | * Identity-Aware Proxy or AWS Application Load Balancer or similar, to front 55 | * your Backstage installation. This sign-in page implementation will silently 56 | * and regularly punch through the proxy to the auth backend to refresh your 57 | * frontend session information, without requiring user interaction. 58 | * 59 | * @public 60 | */ 61 | export const LdapSignInPage = (props: LdapSignInPageProps) => { 62 | const discoveryApi = useApi(discoveryApiRef); 63 | const [username, setUsername] = useState(''); 64 | const [password, setPassword] = useState(''); 65 | const [identity] = useState( 66 | new LdapSignInIdentity({ 67 | provider: props.provider, 68 | discoveryApi, 69 | }) 70 | ); 71 | 72 | const [{ status, error }, { execute }] = useAsync(async () => { 73 | await identity.login({ username, password }); 74 | 75 | props.onSignInSuccess(identity); 76 | }); 77 | 78 | const [{ status: statusRefresh }, { execute: executeRefresh }] = useAsync( 79 | async () => { 80 | await identity.fetch(); 81 | 82 | props.onSignInSuccess(identity); 83 | } 84 | ); 85 | 86 | useEffect(() => { 87 | executeRefresh(); 88 | }, []); 89 | 90 | function onSubmit(u: string, p: string) { 91 | setUsername(u); 92 | setPassword(p); 93 | setTimeout(execute, 0); 94 | } 95 | 96 | if ( 97 | status === 'loading' || 98 | statusRefresh === 'loading' || 99 | statusRefresh === 'not-executed' 100 | ) { 101 | return ; 102 | } else if (status === 'success' || statusRefresh === 'success') { 103 | return null; 104 | } 105 | 106 | function onSignInError(error: Error) { 107 | props?.onSignInError?.(error); 108 | } 109 | 110 | return ( 111 | <> 112 | {props.children} 113 | 119 | 120 | ); 121 | }; 122 | -------------------------------------------------------------------------------- /packages/ldap-auth/README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

@immobiliarelabs/backstage-plugin-ldap-auth

5 | 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier?style=flat-square) 7 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release) 8 | ![license](https://img.shields.io/github/license/immobiliare/backstage-plugin-ldap-auth?style=flat-square) 9 | ![npm (scoped)](https://img.shields.io/npm/v/@immobiliarelabs/backstage-plugin-ldap-auth?style=flat-square) 10 | 11 | > Login page and client-side token management for BAckstage LDAP Authentication Plugin 12 | 13 | This plugin is not meant to be used alone but in pair with: 14 | 15 | - The official [@backstage/plugin-catalog-backend-module-ldap](https://www.npmjs.com/package/@backstage/plugin-catalog-backend-module-ldap) which keeps in sync your LDAP users with Backstage user catalogs! 16 | - Its sibling backend package [@immobiliarelabs/backstage-plugin-ldap-auth-backend](https://www.npmjs.com/package/@immobiliarelabs/backstage-plugin-ldap-auth-backend) 17 | 18 | All the current LTS versions are supported. 19 | 20 | ## Table of Content 21 | 22 | 23 | 24 | - [Installation](#installation) 25 | - [Configuration](#configuration) 26 | - [Powered Apps](#powered-apps) 27 | - [Support & Contribute](#support--contribute) 28 | - [License](#license) 29 | 30 | 31 | 32 | ## Installation 33 | 34 | > These packages are available on npm. 35 | 36 | You can install them in your backstage installation using `yarn workspace` 37 | 38 | ```bash 39 | # install yarn if you don't have it 40 | $ npm install -g yarn 41 | # install frontend plugin 42 | $ yarn workspace app add @immobiliarelabs/backstage-plugin-ldap-auth 43 | # install backend plugin 44 | $ yarn workspace backend add @immobiliarelabs/backstage-plugin-ldap-auth-backend 45 | ``` 46 | 47 | ## Configuration 48 | 49 | > The react components accepts childrens to allow you to customize the login page look and feel 50 | 51 | The component out of the box only shows the form, but you can pass down children components to render your logos/top bar os whatever you want! 52 | 53 |

54 | 55 |

56 | 57 | In the `App.tsx` file, change the `createApp` function adding a `components` with our custom `SignInPage` 58 | 59 | **Note:** This components isn't only UI, it also brings all the token state management and HTTP API calls to the backstage auth routes we already configured in the backend part. 60 | 61 | > `packages/app/src/App.tsx` 62 | 63 | ```tsx 64 | import { LdapAuthFrontendPage } from '@immobiliarelabs/backstage-plugin-ldap-auth'; 65 | 66 | const app = createApp({ 67 | // ... 68 | components: { 69 | SignInPage: (props) => ( 70 | 71 | ), 72 | }, 73 | // ... 74 | }); 75 | ``` 76 | 77 | Now follow instructions at [@immobiliarelabs/backstage-plugin-ldap-auth-backend](../ldap-auth-backend/README.md) to add backend authentication logic! 78 | 79 | ## Powered Apps 80 | 81 | Backstage Plugin LDAP Auth was created by the amazing Node.js team at [ImmobiliareLabs](http://labs.immobiliare.it/), the Tech dept of [Immobiliare.it](https://www.immobiliare.it), the #1 real estate company in Italy. 82 | 83 | We are currently using Backstage Plugin LDAP Auth in our products as well as our internal toolings. 84 | 85 | **If you are using Backstage Plugin LDAP Auth in production [drop us a message](mailto:opensource@immobiliare.it)**. 86 | 87 | ## Support & Contribute 88 | 89 | Made with ❤️ by [ImmobiliareLabs](https://github.com/immobiliare) & [Contributors](https://github.com/immobiliare/backstage-plugin-ldap-auth/CONTRIBUTING.md#contributors) 90 | 91 | We'd love for you to contribute to Backstage Plugin LDAP Auth! 92 | If you have any questions on how to use Backstage Plugin LDAP Auth, bugs and enhancement please feel free to reach out by opening a [GitHub Issue](https://github.com/immobiliare/backstage-plugin-ldap-auth). 93 | 94 | ## License 95 | 96 | Backstage Plugin LDAP Auth is licensed under the MIT license. 97 | See the [LICENSE](https://github.com/immobiliare/backstage-plugin-ldap-auth/LICENSE) file for more information. 98 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Backstage Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * A representation of a successful Backstage sign-in. 19 | * 20 | * Compared to the {@link BackstageIdentityResponse} this type omits 21 | * the decoded identity information embedded in the token. 22 | * 23 | * @public 24 | */ 25 | export interface BackstageSignInResult { 26 | /** 27 | * The token used to authenticate the user within Backstage. 28 | */ 29 | token: string; 30 | } 31 | 32 | /** 33 | * Response object containing the {@link BackstageUserIdentity} and the token 34 | * from the authentication provider. 35 | * 36 | * @public 37 | */ 38 | export interface BackstageIdentityResponse extends BackstageSignInResult { 39 | /** 40 | * A plaintext description of the identity that is encapsulated within the token. 41 | */ 42 | identity: BackstageUserIdentity; 43 | } 44 | 45 | /** 46 | * User identity information within Backstage. 47 | * 48 | * @public 49 | */ 50 | export type BackstageUserIdentity = { 51 | /** 52 | * The type of identity that this structure represents. In the frontend app 53 | * this will currently always be 'user'. 54 | */ 55 | type: 'user'; 56 | 57 | /** 58 | * The entityRef of the user in the catalog. 59 | * For example User:default/sandra 60 | */ 61 | userEntityRef: string; 62 | 63 | /** 64 | * The user and group entities that the user claims ownership through 65 | */ 66 | ownershipEntityRefs: string[]; 67 | }; 68 | 69 | export type LDAPResponse = { 70 | dn: string; 71 | controls?: []; 72 | uid: string; 73 | givenName: string; 74 | cn: string; 75 | uidNumber: string; 76 | gidNumber: string; 77 | homeDirectory: string; 78 | mail: string; 79 | sn: string; 80 | objectClass: string[]; 81 | }; 82 | 83 | export type LDAPUser = Partial; 84 | 85 | export type UserIdentityId = Pick; 86 | 87 | export type BackstageJWTPayload = { 88 | iss: string; 89 | sub: string; 90 | ent: string[]; 91 | aud: string; 92 | iat: number; 93 | exp: number; 94 | }; 95 | 96 | import type { TokenValidator } from './jwt'; 97 | import type { AuthResolverContext } from '@backstage/plugin-auth-node'; 98 | import type { AuthenticationOptions } from 'ldap-authentication'; 99 | import { defaultAuthHandler, defaultSigninResolver } from './auth'; 100 | import { defaultCheckUserExists, defaultLDAPAuthentication } from './ldap'; 101 | 102 | export type CookiesOptions = { 103 | field: string; 104 | secure: boolean; 105 | }; 106 | 107 | export type BackstageLdapAuthConfiguration = { 108 | cookies?: Partial; 109 | ldapAuthenticationOptions: AuthenticationOptions; 110 | }; 111 | 112 | export type Resolvers = { 113 | checkUserExists?: typeof defaultCheckUserExists; 114 | ldapAuthentication?: typeof defaultLDAPAuthentication; 115 | }; 116 | 117 | export type SignInResolver = { 118 | resolver?: typeof defaultSigninResolver; 119 | }; 120 | 121 | export type ProviderCreateOptions = { 122 | // Backstage Provider AuthHandler 123 | authHandler?: typeof defaultAuthHandler; 124 | // Backstage Provider SignInResolver 125 | signIn?: SignInResolver; 126 | 127 | // Custom resolvers 128 | resolvers?: Resolvers; 129 | // Custom validator function for the JWT token if needed 130 | tokenValidator?: TokenValidator; 131 | }; 132 | 133 | export type ProviderConstructor = { 134 | cookies: BackstageLdapAuthConfiguration['cookies']; 135 | ldapAuthenticationOptions: AuthenticationOptions; 136 | authHandler: typeof defaultAuthHandler; 137 | signInResolver: typeof defaultSigninResolver; 138 | checkUserExists: typeof defaultCheckUserExists; 139 | ldapAuthentication: typeof defaultLDAPAuthentication; 140 | resolverContext: AuthResolverContext; 141 | tokenValidator?: TokenValidator; 142 | }; 143 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/components/LoginPage/Form.tsx: -------------------------------------------------------------------------------- 1 | import { Content, Page } from '@backstage/core-components'; 2 | import React, { useEffect, useState } from 'react'; 3 | import { Paper, TextField, Container, Button } from '@material-ui/core'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import PasswordValidator from 'password-validator'; 6 | 7 | export type LoginFormProps = { 8 | onSubmit: (username: string, password: string) => void; 9 | onSignInError?: (error: Error) => void; 10 | error?: Error; 11 | helperTextUsername?: string; 12 | helperTextPassword?: string; 13 | validateUsername?: (usr: string) => boolean; 14 | validatePassword?: (pass: string) => boolean; 15 | usernameLabel?: string; 16 | }; 17 | 18 | const useStyles = makeStyles((theme) => ({ 19 | paper: { 20 | padding: theme.spacing(2), 21 | textAlign: 'center', 22 | color: theme.palette.text.secondary, 23 | }, 24 | paperHead: { 25 | marginBottom: theme.spacing(2), 26 | textAlign: 'center', 27 | color: theme.palette.text.secondary, 28 | }, 29 | })); 30 | 31 | const passwordSchema = new PasswordValidator(); 32 | const usernameSchema = new PasswordValidator(); 33 | 34 | passwordSchema.is().min(4).not().spaces(); 35 | usernameSchema.is().min(4).is().max(40).not().spaces(); 36 | 37 | export const LoginForm = ({ 38 | onSubmit, 39 | onSignInError, 40 | error, 41 | helperTextUsername, 42 | helperTextPassword, 43 | validatePassword, 44 | validateUsername, 45 | usernameLabel, 46 | }: LoginFormProps) => { 47 | const validatePasswd = 48 | validatePassword || passwordSchema.validate.bind(passwordSchema); 49 | const validateUsern = 50 | validateUsername || usernameSchema.validate.bind(usernameSchema); 51 | 52 | const [username, setUsername] = useState(''); 53 | const [password, setPassword] = useState(''); 54 | const [uError, setUError] = useState(Boolean(error)); 55 | const [pError, setPError] = useState(Boolean(error)); 56 | const classes = useStyles(); 57 | 58 | function onClick() { 59 | const isUsernameValid = validateUsern(username) as boolean; 60 | const isPasswordValid = validatePasswd(password) as boolean; 61 | setUError(!isUsernameValid); 62 | setPError(!isPasswordValid); 63 | 64 | if (isUsernameValid && isPasswordValid) onSubmit(username, password); 65 | } 66 | 67 | useEffect(() => { 68 | if (error && onSignInError) { 69 | onSignInError(error); 70 | } 71 | }, [error, onSignInError]); 72 | 73 | useEffect(() => { 74 | const keyDownHandler = (event: { 75 | key: string; 76 | preventDefault: () => void; 77 | }) => { 78 | if (event.key === 'Enter') { 79 | event.preventDefault(); 80 | onClick(); 81 | } 82 | }; 83 | 84 | document.addEventListener('keydown', keyDownHandler); 85 | 86 | return () => { 87 | document.removeEventListener('keydown', keyDownHandler); 88 | }; 89 | }); 90 | 91 | return ( 92 | 93 | 94 | 95 | 96 |
e.preventDefault()}> 97 | setUsername(e.target.value)} 101 | value={username} 102 | type="username" 103 | autoComplete="username" 104 | id="username" 105 | error={uError} 106 | helperText={helperTextUsername} 107 | fullWidth 108 | size="small" 109 | margin="dense" 110 | /> 111 | setPassword(e.target.value)} 115 | value={password} 116 | id="password" 117 | type="password" 118 | autoComplete="password" 119 | error={pError} 120 | helperText={helperTextPassword} 121 | fullWidth 122 | size="small" 123 | margin="dense" 124 | /> 125 | 136 | 137 |
138 |
139 |
140 |
141 | ); 142 | }; 143 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/alpha.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ldapAuthExtensionPoint, 3 | default as ldapAuthModule, 4 | tokenValidatorFactory, 5 | } from './alpha'; 6 | import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; 7 | import request from 'supertest'; 8 | import { COOKIE_FIELD_KEY, JWTTokenValidator } from './jwt'; 9 | import Keyv from 'keyv'; 10 | import { createBackendModule } from '@backstage/backend-plugin-api'; 11 | 12 | describe('ldapProvider new backend system', () => { 13 | it('extension point should work', async () => { 14 | let invalidateTokenMock; 15 | let ldapAuthenticationMock = jest.fn(() => 16 | Promise.resolve({ 17 | dn: 'test', 18 | uid: 'test', 19 | givenName: 'test', 20 | cn: 'test', 21 | uidNumber: 'test', 22 | gidNumber: '42423', 23 | homeDirectory: '/home', 24 | mail: 'test@gmail.com', 25 | sn: 'mock', 26 | }) 27 | ); 28 | 29 | let authHandlerMock = jest.fn(() => 30 | Promise.resolve({ 31 | email: 'test@gmail.com', 32 | displayName: 'test', 33 | }) 34 | ); 35 | 36 | let signIn = jest.fn(() => 37 | Promise.resolve({ 38 | token: 'random', 39 | }) 40 | ); 41 | 42 | const createTokenValidator = () => { 43 | const token = new JWTTokenValidator(new Keyv()); 44 | 45 | invalidateTokenMock = jest.fn(() => Promise.resolve()); 46 | token.invalidateToken = invalidateTokenMock; 47 | return token; 48 | }; 49 | const { server } = await startTestBackend({ 50 | features: [ 51 | import('@backstage/plugin-auth-backend'), 52 | ldapAuthModule, 53 | createBackendModule({ 54 | pluginId: 'auth', 55 | moduleId: 'ldap-ext', 56 | register(reg) { 57 | reg.registerInit({ 58 | deps: { 59 | ldapAuth: ldapAuthExtensionPoint, 60 | }, 61 | async init({ ldapAuth }) { 62 | ldapAuth.set({ 63 | tokenValidator: createTokenValidator(), 64 | resolvers: { 65 | ldapAuthentication: 66 | ldapAuthenticationMock, 67 | }, 68 | authHandler: authHandlerMock as any, 69 | signIn: { resolver: signIn }, 70 | }); 71 | }, 72 | }); 73 | }, 74 | }), 75 | mockServices.rootConfig.factory({ 76 | data: { 77 | app: { 78 | baseUrl: 'http://localhost:3000', 79 | }, 80 | auth: { 81 | providers: { 82 | ldap: { 83 | test: { 84 | cookies: { secure: false }, 85 | ldapAuthenticationOptions: { 86 | usernameAttribute: 'uid', 87 | }, 88 | }, 89 | }, 90 | }, 91 | }, 92 | }, 93 | }), 94 | ], 95 | }); 96 | 97 | const agent = request.agent(server); 98 | await agent 99 | .post('/api/auth/ldap/refresh') 100 | .send({ username: 'hello', password: 'world' }); 101 | 102 | expect(ldapAuthenticationMock).toHaveBeenCalled(); 103 | expect(invalidateTokenMock).not.toHaveBeenCalled(); 104 | 105 | expect(authHandlerMock).toHaveBeenCalled(); 106 | expect(signIn).toHaveBeenCalled(); 107 | }); 108 | 109 | it('service should work', async () => { 110 | let isValidMock = jest.fn(() => Promise.resolve(true)); 111 | const createTokenValidator = () => { 112 | const token = new JWTTokenValidator(new Keyv()); 113 | token.isValid = isValidMock; 114 | return token; 115 | }; 116 | const { server } = await startTestBackend({ 117 | features: [ 118 | import('@backstage/plugin-auth-backend'), 119 | ldapAuthModule, 120 | tokenValidatorFactory({ createTokenValidator }), 121 | mockServices.rootConfig.factory({ 122 | data: { 123 | app: { 124 | baseUrl: 'http://localhost:3000', 125 | }, 126 | auth: { 127 | providers: { 128 | ldap: { 129 | test: { 130 | cookies: { secure: false }, 131 | ldapAuthenticationOptions: { 132 | usernameAttribute: 'uid', 133 | }, 134 | }, 135 | }, 136 | }, 137 | }, 138 | }, 139 | }), 140 | ], 141 | }); 142 | 143 | const agent = request.agent(server); 144 | await agent 145 | .post('/api/auth/ldap/refresh') 146 | .set('Cookie', [`${COOKIE_FIELD_KEY}=eyJqd3QiOiJ`]) 147 | .send({}); 148 | 149 | expect(isValidMock).toHaveBeenCalled(); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/jwt.test.ts: -------------------------------------------------------------------------------- 1 | import { parseJwtPayload, JWTTokenValidator, normalizeTime } from './jwt'; 2 | 3 | import jwt from 'jsonwebtoken'; 4 | import Keyv from 'keyv'; 5 | import { JWT_EXPIRED_TOKEN } from './errors'; 6 | 7 | describe('parseJwtPayload', () => { 8 | it('should parse jwt', () => { 9 | const payload = { sub: 'username' }; 10 | const j = jwt.sign(payload, 'secret', { 11 | expiresIn: '1min', 12 | }); 13 | 14 | const decoded = parseJwtPayload(j); 15 | 16 | expect(decoded?.sub).toBe(payload.sub); 17 | expect(decoded?.iat).toBeDefined(); 18 | expect(decoded?.exp).toBeDefined(); 19 | }); 20 | }); 21 | 22 | describe('Token Validator', () => { 23 | afterEach(() => { 24 | jest.useRealTimers(); 25 | }); 26 | const payload = { sub: 'username' }; 27 | it('should validate', async () => { 28 | const j = jwt.sign(payload, 'secret', { 29 | expiresIn: '1min', 30 | }); 31 | 32 | const validator = new JWTTokenValidator(new Keyv()); 33 | const out = await validator.isValid(j); 34 | expect(out).toBe(true); 35 | }); 36 | 37 | it('should invalidate old token', async () => { 38 | const timer = jest.useFakeTimers(); 39 | const validator = new JWTTokenValidator(new Keyv()); 40 | // Create token 41 | const j = jwt.sign(payload, 'secret', { 42 | expiresIn: '1min', 43 | }); 44 | timer.advanceTimersByTime(1500); 45 | 46 | const out = await validator.isValid(j); 47 | await expect(out).toBe(true); 48 | 49 | await validator.invalidateToken(j); 50 | timer.advanceTimersByTime(1500); 51 | 52 | await expect(validator.isValid(j)).rejects.toEqual( 53 | new Error(JWT_EXPIRED_TOKEN) 54 | ); 55 | }); 56 | 57 | it('invalidating the last tokens should invalidate all tokens', async () => { 58 | const timer = jest.useFakeTimers(); 59 | const validator = new JWTTokenValidator(new Keyv()); 60 | // Create token 61 | const tokens = new Array(10).fill(null).map((_) => { 62 | const out = jwt.sign(payload, 'secret', { 63 | expiresIn: '1min', 64 | }); 65 | timer.advanceTimersByTime(1500); 66 | return out; 67 | }); 68 | const lastToken = jwt.sign(payload, 'secret', { 69 | expiresIn: '1min', 70 | }); 71 | timer.advanceTimersByTime(1500); 72 | 73 | for (const promiseOut of [...tokens, lastToken].map( 74 | validator.isValid.bind(validator) 75 | )) { 76 | await expect(promiseOut).resolves.toEqual(true); 77 | } 78 | timer.advanceTimersByTime(1500); 79 | // Invalidate only last token 80 | await validator.invalidateToken(lastToken); 81 | timer.advanceTimersByTime(1500); 82 | 83 | for (const promiseOut of [...tokens, lastToken].map( 84 | validator.isValid.bind(validator) 85 | )) { 86 | await expect(promiseOut).rejects.toEqual( 87 | new Error(JWT_EXPIRED_TOKEN) 88 | ); 89 | } 90 | }); 91 | 92 | it('tokens should expire', async () => { 93 | const timer = jest.useFakeTimers(); 94 | const validator = new JWTTokenValidator(new Keyv()); 95 | // Create token 96 | const tokens = new Array(10).fill(null).map((_) => { 97 | const out = jwt.sign(payload, 'secret', { 98 | expiresIn: '1min', 99 | }); 100 | timer.advanceTimersByTime(1500); 101 | return out; 102 | }); 103 | timer.advanceTimersByTime(1500); 104 | 105 | for (const promiseOut of tokens.map( 106 | validator.isValid.bind(validator) 107 | )) { 108 | await expect(promiseOut).resolves.toEqual(true); 109 | } 110 | 111 | // tokens should invalid after 1 hour and 1 second 112 | timer.advanceTimersByTime(60 * 60 * 10e3 + 1500); 113 | 114 | for (const promiseOut of tokens.map( 115 | validator.isValid.bind(validator) 116 | )) { 117 | await expect(promiseOut).rejects.toEqual( 118 | new Error(JWT_EXPIRED_TOKEN) 119 | ); 120 | } 121 | }); 122 | 123 | it('should increase token expire time', async () => { 124 | const timer = jest.useFakeTimers(); 125 | const validator = new JWTTokenValidator(new Keyv(), 60 * 60 * 10e3); 126 | // Create token 127 | const tokens = new Array(10).fill(null).map((_) => { 128 | const out = jwt.sign(payload, 'secret', { 129 | expiresIn: '1min', 130 | }); 131 | timer.advanceTimersByTime(1500); 132 | return out; 133 | }); 134 | timer.advanceTimersByTime(1500); 135 | 136 | for (const promiseOut of tokens.map( 137 | validator.isValid.bind(validator) 138 | )) { 139 | await expect(promiseOut).resolves.toEqual(true); 140 | } 141 | 142 | // tokens should invalid after 1 hour and 1 second 143 | timer.advanceTimersByTime(60 * 60 * 10e3 + 1500); 144 | 145 | for (const promiseOut of tokens.map( 146 | validator.isValid.bind(validator) 147 | )) { 148 | await expect(promiseOut).resolves.toEqual(true); 149 | } 150 | 151 | // tokens should invalid after 1 hour and 1 second 152 | timer.advanceTimersByTime(60 * 60 * 10e3 + 1500); 153 | 154 | for (const promiseOut of tokens.map( 155 | validator.isValid.bind(validator) 156 | )) { 157 | await expect(promiseOut).rejects.toEqual( 158 | new Error(JWT_EXPIRED_TOKEN) 159 | ); 160 | } 161 | }); 162 | 163 | it('should logout', async () => { 164 | const timer = jest.useFakeTimers(); 165 | const validator = new JWTTokenValidator(new Keyv()); 166 | 167 | const token = jwt.sign(payload, 'secret', { 168 | expiresIn: '1min', 169 | }); 170 | timer.advanceTimersByTime(1500); 171 | 172 | await expect(validator.isValid(token)).resolves.toEqual(true); 173 | 174 | timer.advanceTimersByTime(1500); 175 | await validator.logout(token, normalizeTime(Date.now())); 176 | 177 | timer.advanceTimersByTime(1500); 178 | 179 | await expect(validator.isValid(token)).rejects.toEqual( 180 | new Error(JWT_EXPIRED_TOKEN) 181 | ); 182 | }); 183 | }); 184 | -------------------------------------------------------------------------------- /examples/validate-api.md: -------------------------------------------------------------------------------- 1 | # Validate API 2 | 3 | If you want to validate Backstage API you have follow this steps: 4 | 5 | 1. Install `keyv`: 6 | 7 | ```bash 8 | yarn add --cwd packages/backend keyv 9 | ``` 10 | 11 | 2. Create the file tokenValidator 12 | 13 | `packages/backend/src/tokenValidator.ts` 14 | 15 | ```ts 16 | import { getRootLogger } from '@backstage/backend-common'; 17 | import { Config } from '@backstage/config'; 18 | import { 19 | JWTTokenValidator, 20 | TokenValidator, 21 | } from '@immobiliarelabs/backstage-plugin-ldap-auth-backend'; 22 | import Keyv from 'keyv'; 23 | 24 | const CONNECTION_STRING = 'backend.database.connection'; 25 | 26 | export default function getTokenValidator( 27 | config: Config 28 | ): TokenValidator | undefined { 29 | const logger = getRootLogger(); 30 | const connection = config.getOptional(CONNECTION_STRING); 31 | 32 | // Here we use postgres to store the tokens 33 | let tokenValidator: TokenValidator | undefined; 34 | if ( 35 | typeof connection === 'object' && 36 | config.getOptional('backend.database.client') === 'pg' 37 | ) { 38 | const host = config.getOptionalString(`${CONNECTION_STRING}.host`); 39 | const port = config.getOptionalString(`${CONNECTION_STRING}.port`); 40 | const user = config.getOptionalString(`${CONNECTION_STRING}.user`); 41 | const password = config.getOptionalString( 42 | `${CONNECTION_STRING}.password` 43 | ); 44 | 45 | const url = `postgresql://${user}:${password}@${host}:${port}/postgres-db`; 46 | 47 | tokenValidator = new JWTTokenValidator( 48 | new Keyv(url, { table: 'token' }) 49 | ); 50 | logger.info('Saving tokens on postgres.'); 51 | } else { 52 | // If postgres is not configured we store tokens in memory 53 | tokenValidator = new JWTTokenValidator(new Keyv()); 54 | logger.warn('The tokens are saved in memory!'); 55 | } 56 | 57 | return tokenValidator; 58 | } 59 | ``` 60 | 61 | 3. Add `TokenValidator` in the plugin env. 62 | 63 | `packages/backend/src/types.ts` 64 | 65 | ```diff 66 | import { Logger } from 'winston'; 67 | import { Config } from '@backstage/config'; 68 | import { 69 | PluginCacheManager, 70 | PluginDatabaseManager, 71 | PluginEndpointDiscovery, 72 | TokenManager, 73 | UrlReader, 74 | } from '@backstage/backend-common'; 75 | import { PluginTaskScheduler } from '@backstage/backend-tasks'; 76 | import { PermissionEvaluator } from '@backstage/plugin-permission-common'; 77 | import { IdentityApi } from '@backstage/plugin-auth-node'; 78 | + import { TokenValidator } from '@immobiliarelabs/backstage-plugin-ldap-auth-backend'; 79 | 80 | export type PluginEnvironment = { 81 | logger: Logger; 82 | cache: PluginCacheManager; 83 | database: PluginDatabaseManager; 84 | config: Config; 85 | reader: UrlReader; 86 | discovery: PluginEndpointDiscovery; 87 | tokenManager: TokenManager; 88 | permissions: PermissionEvaluator; 89 | scheduler: PluginTaskScheduler; 90 | identity: IdentityApi; 91 | + tokenValidator?: TokenValidator; 92 | }; 93 | ``` 94 | 95 | `packages/backend/src/index.ts` 96 | 97 | ```diff 98 | + import getTokenValidator from './tokenValidator'; 99 | ... 100 | function makeCreateEnv(config: Config) { 101 | ... 102 | + const tokenValidator = getTokenValidator(config); 103 | 104 | return (plugin: string): PluginEnvironment => { 105 | const logger = root.child({ type: 'plugin', plugin }); 106 | const database = databaseManager.forPlugin(plugin); 107 | const cache = cacheManager.forPlugin(plugin); 108 | 109 | return { 110 | logger, 111 | cache, 112 | database, 113 | ... 114 | + tokenValidator, 115 | }; 116 | }; 117 | } 118 | ... 119 | ``` 120 | 121 | 4. Create the function `createAuthMiddleware` 122 | 123 | `packages/backend/src/authMiddleware.ts` 124 | 125 | ```ts 126 | import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node'; 127 | import { NextFunction, Request, Response, RequestHandler } from 'express'; 128 | import { PluginEnvironment } from './types'; 129 | 130 | export const createAuthMiddleware = async (appEnv: PluginEnvironment) => { 131 | const authMiddleware: RequestHandler = async ( 132 | req: Request, 133 | res: Response, 134 | next: NextFunction 135 | ) => { 136 | try { 137 | // Here the cookie token is used by ldap-auth-plugin "backstage-token" 138 | const token = 139 | getBearerTokenFromAuthorizationHeader( 140 | req.headers.authorization 141 | ) || (req.cookies?.['backstage-token'] as string | undefined); 142 | if (!token) { 143 | res.status(401).send('Unauthorized'); 144 | return; 145 | } 146 | // If token validator is undefined we do not check 147 | await appEnv.tokenValidator?.isValid(token); 148 | 149 | try { 150 | req.user = await appEnv.identity.getIdentity({ request: req }); 151 | } catch { 152 | // Nope 153 | } 154 | if (!req.headers.authorization) { 155 | // Authorization header may be forwarded by plugin requests 156 | req.headers.authorization = `Bearer ${token}`; 157 | } 158 | next(); 159 | } catch (error) { 160 | res.status(401).send(`Unauthorized ${error}`); 161 | } 162 | }; 163 | return authMiddleware; 164 | }; 165 | ``` 166 | 5. Create the `authMiddleware` and add it to the APIs you want to authenticate. 167 | 168 | `packages/backend/src/index.ts` 169 | ```diff 170 | + import { createAuthMiddleware } from './authMiddleware'; 171 | 172 | async function main() { 173 | ... 174 | + const authMiddleware = await createAuthMiddleware(appEnv); 175 | 176 | const apiRouter = Router(); 177 | apiRouter.use(cookieParser()); 178 | + apiRouter.use('/catalog', authMiddleware, await catalog(catalogEnv)); 179 | + apiRouter.use('/scaffolder', authMiddleware, await scaffolder(scaffolderEnv)); 180 | apiRouter.use('/tech-insights', await techInsights(techInsightsEnv)); 181 | apiRouter.use('/auth', await auth(authEnv)); 182 | + apiRouter.use('/search', authMiddleware, await search(searchEnv)); 183 | + apiRouter.use('/techdocs', authMiddleware, await techdocs(techdocsEnv)); 184 | apiRouter.use('/todo', await todo(todoEnv)); 185 | apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); 186 | apiRouter.use('/kafka', await kafka(kafkaEnv)); 187 | + apiRouter.use('/proxy', authMiddleware, await proxy(proxyEnv)); 188 | apiRouter.use('/badges', await badges(badgesEnv)); 189 | apiRouter.use('/permission', await permission(permissionEnv)); 190 | + apiRouter.use('/gitlab', authMiddleware, await gitlab(gitlabEnv)); 191 | apiRouter.use('/entity-feedback', await entityFeedback(entityFeedbackEnv)); 192 | apiRouter.use(notFoundHandler()); 193 | ... 194 | } 195 | ``` 196 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/provider.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express'; 2 | import { authenticate, AuthenticationOptions } from 'ldap-authentication'; 3 | import type { 4 | BackstageLdapAuthConfiguration, 5 | CookiesOptions, 6 | ProviderConstructor, 7 | ProviderCreateOptions, 8 | UserIdentityId, 9 | } from './types'; 10 | 11 | import { createAuthProviderIntegration } from '@backstage/plugin-auth-backend'; 12 | 13 | import { 14 | AuthProviderRouteHandlers, 15 | AuthResolverContext, 16 | } from '@backstage/plugin-auth-node'; 17 | 18 | import { AuthenticationError } from '@backstage/errors'; 19 | import { AUTH_MISSING_CREDENTIALS, JWT_INVALID_TOKEN } from './errors'; 20 | 21 | import { 22 | parseJwtPayload, 23 | COOKIE_FIELD_KEY, 24 | TokenValidator, 25 | TokenValidatorNoop, 26 | normalizeTime, 27 | JWTTokenValidator, 28 | } from './jwt'; 29 | import { 30 | defaultAuthHandler, 31 | defaultSigninResolver, 32 | prepareBackstageIdentityResponse, 33 | } from './auth'; 34 | 35 | import { defaultCheckUserExists, defaultLDAPAuthentication } from './ldap'; 36 | 37 | export class ProviderLdapAuthProvider implements AuthProviderRouteHandlers { 38 | private readonly checkUserExists: typeof defaultCheckUserExists; 39 | private readonly ldapAuthentication: typeof defaultLDAPAuthentication; 40 | private readonly authHandler: typeof defaultAuthHandler; 41 | private readonly signInResolver: typeof defaultSigninResolver; 42 | private readonly resolverContext: AuthResolverContext; 43 | private readonly jwtValidator: TokenValidator; 44 | private readonly ldapAuthenticationOptions: AuthenticationOptions; 45 | private readonly cookies: CookiesOptions; 46 | 47 | constructor(options: ProviderConstructor) { 48 | this.authHandler = options.authHandler; 49 | this.signInResolver = options.signInResolver; 50 | this.checkUserExists = options.checkUserExists; 51 | this.ldapAuthentication = options.ldapAuthentication; 52 | this.resolverContext = options.resolverContext; 53 | this.ldapAuthenticationOptions = options.ldapAuthenticationOptions; 54 | this.cookies = options.cookies as CookiesOptions; 55 | this.jwtValidator = options.tokenValidator || new TokenValidatorNoop(); 56 | } 57 | 58 | // must keep this methods for the interface 59 | async start() { 60 | return; 61 | } 62 | async frameHandler() { 63 | return; 64 | } 65 | 66 | async check(uid: string): Promise { 67 | const exists = await this.checkUserExists( 68 | { 69 | ...this.ldapAuthenticationOptions, 70 | username: uid, 71 | }, 72 | authenticate 73 | ); 74 | if (!exists) throw new Error(JWT_INVALID_TOKEN); 75 | } 76 | 77 | async refresh(req: Request, res: Response): Promise { 78 | try { 79 | if (req.method !== 'POST') { 80 | throw new AuthenticationError('Method not allowed'); 81 | } 82 | const { username, password } = req.body; 83 | const ctx = this.resolverContext; 84 | const token = req.cookies?.[this.cookies.field]; 85 | 86 | let result: UserIdentityId; 87 | 88 | if (username && password) { 89 | const { uid } = await this.ldapAuthentication( 90 | username, 91 | password, 92 | this.ldapAuthenticationOptions, 93 | authenticate 94 | ); 95 | result = { uid: uid as string }; 96 | } else if (token) { 97 | // this throws if the token is invalid or expired 98 | await this.jwtValidator.isValid(token as string); 99 | 100 | const { sub } = parseJwtPayload(token as string); 101 | 102 | // user is in format `[:][/]` 103 | const uid = sub.split(':').at(-1)!.split('/').at(-1); 104 | await this.check(uid as string); 105 | 106 | result = { uid: uid as string }; 107 | } else { 108 | throw new AuthenticationError(AUTH_MISSING_CREDENTIALS); 109 | } 110 | 111 | // invalidate old token 112 | if (token) await this.jwtValidator.invalidateToken(token); 113 | 114 | // This is used to return a backstage formated profile object 115 | const { profile } = await this.authHandler( 116 | { uid: result.uid as string }, 117 | ctx 118 | ); 119 | 120 | // this sign-in the user into backstage and return an object with the token 121 | const backstageIdentity = await this.signInResolver( 122 | { profile, result }, 123 | ctx 124 | ); 125 | 126 | const response = { 127 | providerInfo: {}, 128 | profile, 129 | // this backstage user information from the token and formats 130 | // the reponse in way that's usable by the FE 131 | backstageIdentity: 132 | prepareBackstageIdentityResponse(backstageIdentity), 133 | }; 134 | 135 | const { exp } = parseJwtPayload(backstageIdentity.token as string); 136 | // maxAge value should be relative to now() 137 | // if it's negative it's expired already 138 | // should not happen but in case it will trigger browser for login page 139 | const maxAge = Math.ceil( 140 | new Date(exp * 1000).valueOf() - 141 | new Date().valueOf() + 142 | ((this.jwtValidator as JWTTokenValidator) 143 | ?.increaseTokenExpireMs || 0) 144 | ); 145 | 146 | res.cookie(this.cookies.field, backstageIdentity.token, { 147 | maxAge, 148 | httpOnly: true, 149 | secure: this.cookies.secure, 150 | }); 151 | 152 | res.json(response); 153 | } catch (e) { 154 | res.clearCookie(this.cookies.field); 155 | throw e; 156 | } 157 | } 158 | 159 | async logout(req: Request, res: Response): Promise { 160 | const token = req.cookies?.[this.cookies.field]; 161 | // this throws if the token is invalid 162 | await this.jwtValidator.isValid(token as string); 163 | 164 | this.jwtValidator.logout(token, normalizeTime(Date.now())); 165 | 166 | res.clearCookie(this.cookies.field); 167 | res.status(200).end(); 168 | } 169 | } 170 | 171 | export const ldap = createAuthProviderIntegration({ 172 | create(options: ProviderCreateOptions) { 173 | return ({ config, resolverContext }) => { 174 | const cnf = config.get( 175 | process.env.NODE_ENV || 'development' 176 | ) as BackstageLdapAuthConfiguration; 177 | 178 | cnf.cookies = { 179 | field: cnf?.cookies?.field || COOKIE_FIELD_KEY, 180 | secure: cnf?.cookies?.secure || false, 181 | }; 182 | 183 | const authHandler = 184 | typeof options?.authHandler === 'function' 185 | ? options?.authHandler 186 | : defaultAuthHandler; 187 | 188 | const signInResolver = 189 | typeof options?.signIn?.resolver === 'function' 190 | ? options?.signIn?.resolver 191 | : defaultSigninResolver; 192 | 193 | // this is LDAP specific 194 | const ldapAuthentication = 195 | typeof options?.resolvers?.ldapAuthentication === 'function' 196 | ? options?.resolvers?.ldapAuthentication 197 | : defaultLDAPAuthentication; 198 | 199 | const checkUserExists = 200 | typeof options?.resolvers?.checkUserExists === 'function' 201 | ? options?.resolvers?.checkUserExists 202 | : defaultCheckUserExists; 203 | 204 | return new ProviderLdapAuthProvider({ 205 | ldapAuthenticationOptions: cnf.ldapAuthenticationOptions, 206 | cookies: cnf.cookies, 207 | authHandler, 208 | signInResolver, 209 | checkUserExists, 210 | ldapAuthentication, 211 | resolverContext, 212 | tokenValidator: options.tokenValidator, 213 | }); 214 | }; 215 | }, 216 | }); 217 | -------------------------------------------------------------------------------- /packages/ldap-auth/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [4.3.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.1-alpha.0...v4.3.1) (2024-10-18) 7 | 8 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 9 | 10 | ## [4.3.1-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.0...v4.3.1-alpha.0) (2024-10-18) 11 | 12 | ### Bug Fixes 13 | 14 | - password printed in console ([e58c353](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/e58c3539a784909284f56b91740720f95b43f61f)) 15 | 16 | # [4.3.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.0-alpha.1...v4.3.0) (2024-07-23) 17 | 18 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 19 | 20 | # [4.3.0-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.0-alpha.0...v4.3.0-alpha.1) (2024-07-23) 21 | 22 | ### Bug Fixes 23 | 24 | - **backstage:** pluginPackage and pluginId ([1284e95](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/1284e9577924652f9c6c688a62b4d59ff09f7512)) 25 | 26 | # [4.3.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.2.0...v4.3.0-alpha.0) (2024-07-23) 27 | 28 | ### Features 29 | 30 | - bump dependecies and remove deprecations ([918c021](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/918c02115a5f1c9e65b10a797a16775779420e90)) 31 | 32 | # [4.2.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.2.0-alpha.0...v4.2.0) (2024-03-26) 33 | 34 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 35 | 36 | # [4.2.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.1.0...v4.2.0-alpha.0) (2024-03-25) 37 | 38 | ### Features 39 | 40 | - Support for Signin Error; Parameterizing Username label ([84941fa](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/84941fae0c1d42e82edda5872dfd37ca5970666e)) 41 | 42 | # [4.1.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.1.0-alpha.1...v4.1.0) (2024-03-11) 43 | 44 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 45 | 46 | # [4.1.0-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.1.0-alpha.0...v4.1.0-alpha.1) (2024-03-07) 47 | 48 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 49 | 50 | # [4.1.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.0.0...v4.1.0-alpha.0) (2024-03-05) 51 | 52 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 53 | 54 | # [4.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.0.0-alpha.0...v4.0.0) (2023-09-22) 55 | 56 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 57 | 58 | # [4.0.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.2-alpha.0...v4.0.0-alpha.0) (2023-09-22) 59 | 60 | ### Bug Fixes 61 | 62 | - removed parentheses being rendered after LoginForm component ([1604b78](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/1604b78d51e2e556fd8c513fbd62b85bc018b5aa)) 63 | 64 | ## [3.0.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.1...v3.0.2-alpha.0) (2023-04-18) 65 | 66 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 67 | 68 | ## [3.0.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.1-alpha.1...v3.0.1) (2023-03-10) 69 | 70 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 71 | 72 | ## [3.0.1-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.1-alpha.0...v3.0.1-alpha.1) (2023-03-10) 73 | 74 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 75 | 76 | ## [3.0.1-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.0...v3.0.1-alpha.0) (2023-03-08) 77 | 78 | ### Bug Fixes 79 | 80 | - packages/ldap-auth/package.json to reduce vulnerabilities ([76be63c](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/76be63cefb2a0a5b7b11f9d62ba9d1e7b9004e2c)) 81 | 82 | # [3.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.0-alpha.0...v3.0.0) (2023-01-16) 83 | 84 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 85 | 86 | # [3.0.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.4...v3.0.0-alpha.0) (2023-01-13) 87 | 88 | ### Features 89 | 90 | - the children parameter type of LdapSignInPageProps type is now a ReactNode ([1d9632f](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/1d9632f55ce32888c9a5cc28659629582d126d99)) 91 | 92 | ### BREAKING CHANGES 93 | 94 | - the children parameter type of LdapSignInPageProps type is now a ReactNode 95 | 96 | ## [2.0.4](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.3...v2.0.4) (2022-12-07) 97 | 98 | ## [2.0.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1...v2.0.2-alpha.0) (2022-11-29) 99 | 100 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 101 | 102 | ## [2.0.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1...v2.0.2-alpha.0) (2022-11-29) 103 | 104 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 105 | 106 | ## [2.0.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1-alpha.2...v2.0.1) (2022-11-23) 107 | 108 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 109 | 110 | ## [2.0.1-alpha.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1-alpha.1...v2.0.1-alpha.2) (2022-11-23) 111 | 112 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 113 | 114 | ## [2.0.1-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1-alpha.0...v2.0.1-alpha.1) (2022-11-22) 115 | 116 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 117 | 118 | ## [2.0.1-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.0...v2.0.1-alpha.0) (2022-11-22) 119 | 120 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 121 | 122 | # [2.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.3...v2.0.0) (2022-11-22) 123 | 124 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 125 | 126 | ## [1.1.3](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.2...v1.1.3) (2022-11-11) 127 | 128 | # [2.0.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.2-alpha.0...v2.0.0-alpha.0) (2022-11-17) 129 | 130 | ### Bug Fixes 131 | 132 | - allow no children to the LdapAuthFrontendPage ([7624203](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/7624203260d6201815a1f8c79a4be98ba880cb89)) 133 | 134 | # [1.1.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.4...v1.1.0) (2022-09-30) 135 | 136 | ### Features 137 | 138 | - exports more utility functions ([3e00432](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/3e00432e1b11da04ff42f534a794fdce29a1db28)) 139 | 140 | ## [1.0.4](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.4-alpha.2...v1.0.4) (2022-09-28) 141 | 142 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 143 | 144 | ## [1.0.4-alpha.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.4-alpha.1...v1.0.4-alpha.2) (2022-09-28) 145 | 146 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 147 | 148 | ## [1.0.4-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.3...v1.0.4-alpha.1) (2022-09-28) 149 | 150 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 151 | 152 | ## [1.0.4-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.3...v1.0.4-alpha.0) (2022-09-22) 153 | 154 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 155 | 156 | ## [1.0.3](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.2...v1.0.3) (2022-09-14) 157 | 158 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 159 | 160 | ## [1.0.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.1...v1.0.2) (2022-09-05) 161 | 162 | ### Bug Fixes 163 | 164 | - package.json repository field ([6dc00dd](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/6dc00dd26eb1d81c3ddc9b354faa7fedd21b49aa)) 165 | 166 | ## [1.0.1](https://github.com/simonecorsi/backstage-plugin-ldap-auth/compare/v1.0.0...v1.0.1) (2022-09-05) 167 | 168 | ### Bug Fixes 169 | 170 | - readme screenshot url ([c3fab29](https://github.com/simonecorsi/backstage-plugin-ldap-auth/commit/c3fab297f3fe8772d1fbed81ca136f0ed8246c14)) 171 | 172 | # [1.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.0-alpha.1...v1.0.0) (2022-09-05) 173 | 174 | ### Features 175 | 176 | - added custom validate functions ([8994ec4](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/8994ec4cdee76a617b53e6c585367604ea1041a3)) 177 | - **frontend:** added helperText ([3060996](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/30609964082f8de9496eb75d9216b70da860aac0)) 178 | 179 | # [1.0.0-alpha.1](https://github.com/immobiliare/backstage-ldap-auth/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-08-26) 180 | 181 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 182 | -------------------------------------------------------------------------------- /packages/ldap-auth/src/components/LoginPage/Identity.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Backstage Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | BackstageUserIdentity, 19 | discoveryApiRef, 20 | IdentityApi, 21 | ProfileInfo, 22 | } from '@backstage/core-plugin-api'; 23 | import { ResponseError } from '@backstage/errors'; 24 | import { LdapSession, ldapSessionSchema } from './types'; 25 | 26 | export const DEFAULTS = { 27 | // The amount of time between token refreshes, if we fail to get an actual 28 | // value out of the exp claim 29 | defaultTokenExpiryMillis: 5 * 60 * 1000, 30 | // The amount of time before the actual expiry of the Backstage token, that we 31 | // shall start trying to get a new one 32 | tokenExpiryMarginMillis: 5 * 60 * 1000, 33 | } as const; 34 | 35 | // When the token expires, with some margin 36 | export function tokenToExpiry(jwtToken: string | undefined): Date { 37 | const fallback = new Date(Date.now() + DEFAULTS.defaultTokenExpiryMillis); 38 | if (!jwtToken) { 39 | return fallback; 40 | } 41 | 42 | const [_header, rawPayload, _signature] = jwtToken.split('.'); 43 | const payload = JSON.parse(atob(rawPayload)); 44 | if (typeof payload.exp !== 'number') { 45 | return fallback; 46 | } 47 | 48 | return new Date(payload.exp * 1000 - DEFAULTS.tokenExpiryMarginMillis); 49 | } 50 | 51 | type ProxiedSignInIdentityOptions = { 52 | provider: string; 53 | discoveryApi: typeof discoveryApiRef.T; 54 | }; 55 | 56 | type State = 57 | | { 58 | type: 'empty'; 59 | } 60 | | { 61 | type: 'fetching'; 62 | promise: Promise; 63 | previous: LdapSession | undefined; 64 | } 65 | | { 66 | type: 'active'; 67 | session: LdapSession; 68 | expiresAt: Date; 69 | } 70 | | { 71 | type: 'failed'; 72 | error: Error; 73 | }; 74 | 75 | export type Auth = { username: string; password: string }; 76 | 77 | /** 78 | * An identity API that gets the user auth information solely based on a 79 | * provider's `/refresh` endpoint. 80 | */ 81 | export class LdapSignInIdentity implements IdentityApi { 82 | private readonly options: ProxiedSignInIdentityOptions; 83 | private readonly abortController: AbortController; 84 | private state: State; 85 | 86 | constructor(options: ProxiedSignInIdentityOptions) { 87 | this.options = options; 88 | this.abortController = new AbortController(); 89 | this.state = { type: 'empty' }; 90 | } 91 | 92 | async login(auth: Auth) { 93 | // Try to make a first fetch, bubble up any errors to the caller 94 | await this.loginAsync(auth); 95 | } 96 | 97 | async fetch(forceRefresh?: boolean) { 98 | await this.getSessionAsync(forceRefresh); 99 | } 100 | 101 | /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getUserId} */ 102 | getUserId(): string { 103 | const { backstageIdentity } = this.getSessionSync(); 104 | const ref = backstageIdentity.identity.userEntityRef; 105 | const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref); 106 | if (!match) { 107 | throw new TypeError(`Invalid user entity reference "${ref}"`); 108 | } 109 | 110 | return match[3]; 111 | } 112 | 113 | /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getIdToken} */ 114 | async getIdToken(): Promise { 115 | const session = await this.getSessionAsync(); 116 | return session.backstageIdentity.token; 117 | } 118 | 119 | /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfile} */ 120 | getProfile(): ProfileInfo { 121 | const session = this.getSessionSync(); 122 | return session.profile; 123 | } 124 | 125 | /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getProfileInfo} */ 126 | async getProfileInfo(): Promise { 127 | const session = await this.getSessionAsync(); 128 | return session.profile; 129 | } 130 | 131 | /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getBackstageIdentity} */ 132 | async getBackstageIdentity(): Promise { 133 | const session = await this.getSessionAsync(); 134 | return session.backstageIdentity.identity; 135 | } 136 | 137 | /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.getCredentials} */ 138 | async getCredentials(): Promise<{ token?: string | undefined }> { 139 | const session = await this.getSessionAsync(); 140 | return { 141 | token: session.backstageIdentity.token, 142 | }; 143 | } 144 | 145 | /** {@inheritdoc @backstage/core-plugin-api#IdentityApi.signOut} */ 146 | async signOut(): Promise { 147 | this.abortController.abort(); 148 | const token = await this.getIdToken(); 149 | const baseUrl = await this.options.discoveryApi.getBaseUrl('auth'); 150 | await fetch(`${baseUrl}/${this.options.provider}/logout`, { 151 | method: 'POST', 152 | headers: { 'Content-Type': 'application/json' }, 153 | credentials: 'include', 154 | body: JSON.stringify({ token: token as string }), 155 | }); 156 | } 157 | 158 | getSessionSync(): LdapSession { 159 | if (this.state.type === 'active') { 160 | return this.state.session; 161 | } else if (this.state.type === 'fetching' && this.state.previous) { 162 | return this.state.previous; 163 | } 164 | throw new Error( 165 | 'No session available. Try reloading your browser page.' 166 | ); 167 | } 168 | 169 | async getSessionAsync(forceRefresh?: boolean): Promise { 170 | if (this.state.type === 'fetching') { 171 | return this.state.promise; 172 | } else if ( 173 | this.state.type === 'active' && 174 | new Date() < this.state.expiresAt && 175 | !forceRefresh 176 | ) { 177 | return this.state.session; 178 | } 179 | 180 | const previous = 181 | this.state.type === 'active' ? this.state.session : undefined; 182 | 183 | const promise = this.fetchSession().then( 184 | (session) => { 185 | this.state = { 186 | type: 'active', 187 | session, 188 | expiresAt: tokenToExpiry(session.backstageIdentity.token), 189 | }; 190 | return session; 191 | }, 192 | (error) => { 193 | this.state = { 194 | type: 'failed', 195 | error, 196 | }; 197 | throw error; 198 | } 199 | ); 200 | 201 | this.state = { 202 | type: 'fetching', 203 | promise, 204 | previous, 205 | }; 206 | 207 | return promise; 208 | } 209 | 210 | async loginAsync(auth: Auth): Promise { 211 | if (this.state.type === 'fetching') { 212 | return this.state.promise; 213 | } else if ( 214 | this.state.type === 'active' && 215 | new Date() < this.state.expiresAt 216 | ) { 217 | return this.state.session; 218 | } 219 | 220 | const previous = 221 | this.state.type === 'active' ? this.state.session : undefined; 222 | 223 | const promise = this.fetchSessionWithAuth(auth).then( 224 | (session) => { 225 | this.state = { 226 | type: 'active', 227 | session, 228 | expiresAt: tokenToExpiry(session.backstageIdentity.token), 229 | }; 230 | return session; 231 | }, 232 | (error) => { 233 | this.state = { 234 | type: 'failed', 235 | error, 236 | }; 237 | throw error; 238 | } 239 | ); 240 | 241 | this.state = { 242 | type: 'fetching', 243 | promise, 244 | previous, 245 | }; 246 | 247 | return promise; 248 | } 249 | 250 | async fetchSessionWithAuth(auth: Auth): Promise { 251 | const baseUrl = await this.options.discoveryApi.getBaseUrl('auth'); 252 | 253 | // Note that we do not use the fetchApi here, since this all happens before 254 | // sign-in completes so there can be no automatic token injection and 255 | // similar. 256 | const response = await fetch( 257 | `${baseUrl}/${this.options.provider}/refresh`, 258 | { 259 | method: 'POST', 260 | signal: this.abortController.signal, 261 | headers: { 262 | 'x-requested-with': 'XMLHttpRequest', 263 | 'Content-Type': 'application/json', 264 | }, 265 | credentials: 'include', 266 | body: JSON.stringify(auth), 267 | } 268 | ); 269 | 270 | if (!response.ok) { 271 | throw await ResponseError.fromResponse(response); 272 | } 273 | 274 | return ldapSessionSchema.parse(await response.json()); 275 | } 276 | 277 | async fetchSession(): Promise { 278 | const baseUrl = await this.options.discoveryApi.getBaseUrl('auth'); 279 | 280 | // Note that we do not use the fetchApi here, since this all happens before 281 | // sign-in completes so there can be no automatic token injection and 282 | // similar. 283 | const response = await fetch( 284 | `${baseUrl}/${this.options.provider}/refresh`, 285 | { 286 | method: 'POST', 287 | signal: this.abortController.signal, 288 | headers: { 'x-requested-with': 'XMLHttpRequest' }, 289 | credentials: 'include', 290 | } 291 | ); 292 | 293 | if (!response.ok) { 294 | throw await ResponseError.fromResponse(response); 295 | } 296 | 297 | return ldapSessionSchema.parse(await response.json()); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [4.3.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.1-alpha.0...v4.3.1) (2024-10-18) 7 | 8 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 9 | 10 | ## [4.3.1-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.0...v4.3.1-alpha.0) (2024-10-18) 11 | 12 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 13 | 14 | # [4.3.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.0-alpha.1...v4.3.0) (2024-07-23) 15 | 16 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 17 | 18 | # [4.3.0-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.0-alpha.0...v4.3.0-alpha.1) (2024-07-23) 19 | 20 | ### Bug Fixes 21 | 22 | - **backstage:** pluginPackage and pluginId ([1284e95](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/1284e9577924652f9c6c688a62b4d59ff09f7512)) 23 | 24 | # [4.3.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.2.0...v4.3.0-alpha.0) (2024-07-23) 25 | 26 | ### Bug Fixes 27 | 28 | - Improve error message for no user details returned ([d28c7f4](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/d28c7f4606d9a85abf7a95bb339d3e019ed8f007)), closes [#700](https://github.com/immobiliare/backstage-plugin-ldap-auth/issues/700) 29 | - tests ([4d2863c](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/4d2863c1845c593a790803153c3cdd80639f473a)), closes [#700](https://github.com/immobiliare/backstage-plugin-ldap-auth/issues/700) 30 | 31 | ### Features 32 | 33 | - bump dependecies and remove deprecations ([918c021](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/918c02115a5f1c9e65b10a797a16775779420e90)) 34 | 35 | # [4.2.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.2.0-alpha.0...v4.2.0) (2024-03-26) 36 | 37 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 38 | 39 | # [4.2.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.1.0...v4.2.0-alpha.0) (2024-03-25) 40 | 41 | ### Features 42 | 43 | - upgrade auth backend ([e05c6b6](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/e05c6b605e060079dfb920289cc66ab4f55a3087)) 44 | 45 | # [4.1.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.1.0-alpha.1...v4.1.0) (2024-03-11) 46 | 47 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 48 | 49 | # [4.1.0-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.1.0-alpha.0...v4.1.0-alpha.1) (2024-03-07) 50 | 51 | ### Features 52 | 53 | - refactored code API and added docs ([98b384c](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/98b384c407adb0a70a8b954181d1a8be3e63184c)) 54 | 55 | # [4.1.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.0.0...v4.1.0-alpha.0) (2024-03-05) 56 | 57 | ### Features 58 | 59 | - support for new backend system ([d711af4](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/d711af4ef7f5a95f9d53fbea8c5f67d0583fd0f3)) 60 | 61 | # [4.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.0.0-alpha.0...v4.0.0) (2023-09-22) 62 | 63 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 64 | 65 | # [4.0.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.2-alpha.0...v4.0.0-alpha.0) (2023-09-22) 66 | 67 | ### Features 68 | 69 | - upgrade auth packages ([75f673e](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/75f673ee408cc51b610dc6dd11862a2731490372)) 70 | 71 | ### BREAKING CHANGES 72 | 73 | - This upgrade breaks the types with all Backstage versions minor of 1.18 74 | 75 | ## [3.0.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.1...v3.0.2-alpha.0) (2023-04-18) 76 | 77 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 78 | 79 | ## [3.0.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.1-alpha.1...v3.0.1) (2023-03-10) 80 | 81 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 82 | 83 | ## [3.0.1-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.1-alpha.0...v3.0.1-alpha.1) (2023-03-10) 84 | 85 | ### Bug Fixes 86 | 87 | - **backend:** authenticate function is undefined ([6a561d0](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/6a561d046b3bcba97ad84fd5a69a21c770bc55b9)) 88 | 89 | ## [3.0.1-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.0...v3.0.1-alpha.0) (2023-03-08) 90 | 91 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 92 | 93 | # [3.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.0-alpha.0...v3.0.0) (2023-01-16) 94 | 95 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 96 | 97 | # [3.0.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.4...v3.0.0-alpha.0) (2023-01-13) 98 | 99 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 100 | 101 | ## [2.0.4](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.3...v2.0.4) (2022-12-07) 102 | 103 | ## [2.0.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1...v2.0.2-alpha.0) (2022-11-29) 104 | 105 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 106 | 107 | ## [2.0.3](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.2...v2.0.3) (2022-12-07) 108 | 109 | ### Bug Fixes 110 | 111 | - default cookie field ([60cdd39](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/60cdd39e7b6de63757e106dd61090152900d9868)), closes [#142](https://github.com/immobiliare/backstage-plugin-ldap-auth/issues/142) 112 | - removes ?? for || ([2c30828](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/2c30828bb815c87f12b3a3471ff7bfa81454e878)) 113 | 114 | ## [2.0.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1...v2.0.2) (2022-11-29) 115 | 116 | ## [2.0.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1...v2.0.2-alpha.0) (2022-11-29) 117 | 118 | ### Bug Fixes 119 | 120 | - documentation typo ([ca575c0](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/ca575c0c1bc351c63a0f0c6d97695cc22ac968ed)) 121 | 122 | ## [2.0.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1-alpha.2...v2.0.1) (2022-11-23) 123 | 124 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 125 | 126 | ## [2.0.1-alpha.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1-alpha.1...v2.0.1-alpha.2) (2022-11-23) 127 | 128 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 129 | 130 | ## [2.0.1-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1-alpha.0...v2.0.1-alpha.1) (2022-11-22) 131 | 132 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 133 | 134 | # [2.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.3...v2.0.0) (2022-11-22) 135 | 136 | ### Bug Fixes 137 | 138 | - **ldap.ts:** default auth function signature ([9b7c25a](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/9b7c25a73d5eff55dce78f892f63e33c243dcd90)) 139 | 140 | ### Features 141 | 142 | - reworked options and injectables ([3ecc9cb](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/3ecc9cb7cb111a5ac24981ab7eb8879809ad860a)) 143 | 144 | ### BREAKING CHANGES 145 | 146 | - Structure and options 147 | 148 | # [2.0.0-alpha.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2022-11-21) 149 | 150 | ### Bug Fixes 151 | 152 | - **ldap.ts:** default auth function signature ([3f2dd02](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/3f2dd0270dd1fd137d28a81101943d0014bf32bc)) 153 | 154 | # [2.0.0-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2022-11-21) 155 | 156 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 157 | 158 | # [2.0.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.2-alpha.0...v2.0.0-alpha.0) (2022-11-17) 159 | 160 | ### Features 161 | 162 | - reworked options and injectables ([b6b1445](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/b6b144539827301a7c95d6547d0217a1fe3cc103)) 163 | 164 | ### BREAKING CHANGES 165 | 166 | - Structure and options 167 | 168 | ## [1.1.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.1...v1.1.2-alpha.0) (2022-10-28) 169 | 170 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 171 | 172 | ## [1.1.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.0...v1.1.1) (2022-10-03) 173 | 174 | ### Bug Fixes 175 | 176 | - increase token expire ms ([1f92d24](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/1f92d2449fed2d630ac375670834c70f8862243e)) 177 | 178 | # [1.1.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.4...v1.1.0) (2022-09-30) 179 | 180 | ### Features 181 | 182 | - added possibility to increase token expire time ([e7130d8](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/e7130d85bfa36c1ad17581e628bb57ab1dfd0f65)) 183 | - exports more utility functions ([3e00432](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/3e00432e1b11da04ff42f534a794fdce29a1db28)) 184 | 185 | ## [1.0.4](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.4-alpha.2...v1.0.4) (2022-09-28) 186 | 187 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 188 | 189 | ## [1.0.4-alpha.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.4-alpha.1...v1.0.4-alpha.2) (2022-09-28) 190 | 191 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 192 | 193 | ## [1.0.4-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.3...v1.0.4-alpha.1) (2022-09-28) 194 | 195 | ### Bug Fixes 196 | 197 | - packages/ldap-auth-backend/package.json to reduce vulnerabilities ([aab30fe](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/aab30fe30f5f2ba0269770b3476e64f7f299b60b)) 198 | 199 | ## [1.0.3](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.2...v1.0.3) (2022-09-14) 200 | 201 | ### Bug Fixes 202 | 203 | - set cookie paired with rejectUnauthorized ([bc020ec](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/bc020ecd3bd10b1e952d4aa43a55a2ac00e4dea9)) 204 | 205 | ## [1.0.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.1...v1.0.2) (2022-09-05) 206 | 207 | ### Bug Fixes 208 | 209 | - package.json repository field ([6dc00dd](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/6dc00dd26eb1d81c3ddc9b354faa7fedd21b49aa)) 210 | 211 | ## [1.0.1](https://github.com/simonecorsi/backstage-plugin-ldap-auth/compare/v1.0.0...v1.0.1) (2022-09-05) 212 | 213 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 214 | 215 | # [1.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.0-alpha.1...v1.0.0) (2022-09-05) 216 | 217 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 218 | 219 | # [1.0.0-alpha.1](https://github.com/immobiliare/backstage-ldap-auth/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-08-26) 220 | 221 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth-backend 222 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/README.md: -------------------------------------------------------------------------------- 1 |

2 | logo 3 |

4 |

@immobiliarelabs/backstage-plugin-ldap-auth-backend

5 | 6 | ![npm (scoped)](https://img.shields.io/npm/v/@immobiliarelabs/backstage-plugin-ldap-auth-backend?style=flat-square) 7 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier?style=flat-square) 8 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release) 9 | ![license](https://img.shields.io/github/license/immobiliare/backstage-plugin-ldap-auth?style=flat-square) 10 | 11 | > LDAP Authentication your [Backstage](https://backstage.io/) deployment 12 | 13 | This package is the Backend Provider to add LDAP authentication to your Backstage instance! 14 | 15 | - Customizable: Authentication request format and marshaling of the response can be injected with custom ones; 16 | - Works on simple stand-alone process or scaled infrastracture spanning multiple deployments using the shared PostgreSQL instance that Backstage already uses; 17 | 18 | This plugin is not meant to be used alone but in pair with: 19 | 20 | - The official [@backstage/plugin-catalog-backend-module-ldap](https://www.npmjs.com/package/@backstage/plugin-catalog-backend-module-ldap) which keeps in sync your LDAP users with Backstage user catalogs! 21 | - Its sibling frontend package [@immobiliarelabs/backstage-plugin-ldap-auth](https://www.npmjs.com/package/@immobiliarelabs/backstage-plugin-ldap-auth) 22 | 23 | All the current LTS versions are supported. 24 | 25 | ## Table of Content 26 | 27 | 28 | 29 | - [Installation](#installation) 30 | - [Configurations](#configurations) 31 | - [Setup](#setup) 32 | - [Connection Configuration](#connection-configuration) 33 | - [Add the authentication backend plugin](#add-the-authentication-backend-plugin) 34 | - [Custom LDAP Configurations](#custom-ldap-configurations) 35 | - [Custom authentication function](#custom-authentication-function) 36 | - [Custom check if user exists](#custom-check-if-user-exists) 37 | - [Add the login form](#add-the-login-form) 38 | - [Powered Apps](#powered-apps) 39 | - [Support & Contribute](#support--contribute) 40 | - [License](#license) 41 | 42 | 43 | 44 | ## Installation 45 | 46 | > These packages are available on npm. 47 | 48 | You can install them in your backstage installation using `yarn workspace` 49 | 50 | ```bash 51 | # install yarn if you don't have it 52 | $ npm install -g yarn 53 | # install backend plugin 54 | $ yarn workspace backend add @immobiliarelabs/backstage-plugin-ldap-auth-backend 55 | # install frontend plugin 56 | $ yarn workspace app add @immobiliarelabs/backstage-plugin-ldap-auth 57 | ``` 58 | 59 | ## Configurations 60 | 61 | > This documentation assumes that you have already scaffolded your Backstage instance from the official `@backstage/create-app`, all files that we're going to customize here are the one already created by the CLI! 62 | 63 | ### Setup 64 | 65 | If you didn't have already, you need to configure Backstage's official LDAP plugin, that is needed to import and keep in syncs users your LDAP users. 66 | 67 | ```sh 68 | # in your backstage repo 69 | yarn add @backstage/plugin-catalog-backend-module-ldap 70 | ``` 71 | 72 | and follow this [guide](https://backstage.io/docs/integrations/ldap/org) 73 | 74 | ### Connection Configuration 75 | 76 | > Adds connection configuration inside your backstage YAML config file, eg: `app-config.yaml` 77 | 78 | We use [`ldap-authentication`](https://github.com/shaozi/ldap-authentication) for authentication, you can find all the configurations at this [link], `ldapOpts` fields are options provided to lower level ldap client read more at [`ldapjs` ](https://github.com/ldapjs/node-ldapjs) 79 | 80 | > Add in you You backstage configuration file 81 | 82 | ```yml 83 | auth: 84 | environment: { ENV_NAME } # eg: production|staging|review|develop 85 | providers: 86 | ldap: 87 | # eg: production|staging|review|develop 88 | { ENV_NAME }: 89 | cookies: 90 | secure: false # https cookies or not 91 | field: 'backstage-token' # default 92 | 93 | ldapAuthenticationOptions: 94 | userSearchBase: 'ou=users,dc=ns,dc=farm' # REQUIRED 95 | # what is the user unique key in your ldap instance 96 | usernameAttribute: 'uid' # defaults to `uid` 97 | # directory where to search user 98 | # default search will be `[userSearchBase]=[username],[userSearchBase]` 99 | 100 | # User able to list other users, this is used 101 | # to check incoming JWT if user are already part of the LDAP 102 | # NOTE: If no admin user/pass provided we'll attempt a credential-less search 103 | adminDn: uid={ADMIN_USERNAME},ou=users,dc=ns,dc=farm 104 | adminPassword: '' 105 | 106 | ldapOpts: 107 | url: 108 | - 'ldaps://123.123.123.123' 109 | tlsOptions: 110 | rejectUnauthorized: false 111 | ``` 112 | 113 | ### Add the authentication backend plugin 114 | 115 | This is for a basic usage: - single process - No custom auth or user object marshaling - in-memory sessions 116 | 117 | For more uses cases you can see the [example folders](https://github.com/immobiliare/backstage-plugin-ldap-auth/tree/main/examples) 118 | 119 | > `packages/backend/src/plugins/auth.ts` 120 | 121 | ```ts 122 | const backend = createBackend(); 123 | 124 | // This is required to work 125 | backend.add(import('@backstage/plugin-auth-backend')); 126 | ... 127 | backend.add(import('@backstage/plugin-auth-backend')); 128 | backend.add(import('@immobiliarelabs/backstage-plugin-ldap-auth-backend')); 129 | ... 130 | backend.start(); 131 | 132 | ``` 133 | 134 | If you want to connect to Postgres for the store of the token (default is in memory): 135 | 136 | ```ts 137 | // index.ts 138 | import { tokenValidatorFactory } from '@immobiliarelabs/backstage-plugin-ldap-auth-backend'; 139 | 140 | // This is required to work 141 | backend.add(import('@backstage/plugin-auth-backend')); 142 | ... 143 | backend.add(import('@immobiliarelabs/backstage-plugin-ldap-auth-backend')); 144 | backend.add(tokenValidatorFactory({ createTokenValidator })); 145 | ... 146 | backend.start(); 147 | 148 | 149 | // auth.ts 150 | createTokenValidator(config: Config): TokenValidator { 151 | ... 152 | const url = `postgresql://${user}:${password}@${host}:${port}/mydb`; 153 | return new JWTTokenValidator( 154 | new Keyv(url, { table: 'token' }) 155 | ); 156 | } 157 | 158 | ``` 159 | 160 | ### Custom LDAP Configurations 161 | 162 | If your LDAP server connection options requires more fine tune than we handle here you can inject your custom auth function, take a look at `ldap.create` types at `resolvers.ldapAuthentication`, you can copy the default function and change what you need! 163 | 164 | This can be also done for the `resolvers.checkUserExists` function, which runs when controlling a JWT token. 165 | 166 | #### Custom authentication function 167 | 168 | ```ts 169 | import { 170 | coreServices, 171 | createBackendModule, 172 | } from '@backstage/backend-plugin-api'; 173 | import { ldapAuthExtensionPoint } from '@immobiliarelabs/backstage-plugin-ldap-auth-backend'; 174 | 175 | export default createBackendModule({ 176 | pluginId: 'auth', 177 | moduleId: 'ldap-ext', 178 | register(reg) { 179 | reg.registerInit({ 180 | deps: { 181 | config: coreServices.rootConfig, 182 | ldapAuth: ldapAuthExtensionPoint, 183 | }, 184 | async init({ config, ldapAuth }) { 185 | ldapAuth.set({ 186 | resolvers: { 187 | async ldapAuthentication( 188 | username, 189 | password, 190 | ldapOptions, 191 | authFunction 192 | ): LDAPUser { 193 | // modify your ldapOptions and do whatever you need to format it 194 | // ... 195 | const user = await authFunction(ldapOptions); 196 | return { uid: user.uid }; 197 | }, 198 | }, 199 | }); 200 | }, 201 | }); 202 | }, 203 | }); 204 | ``` 205 | 206 | #### Custom check if user exists 207 | 208 | ```ts 209 | export default createBackendModule({ 210 | pluginId: 'auth', 211 | moduleId: 'ldap-ext', 212 | register(reg) { 213 | reg.registerInit({ 214 | deps: { 215 | config: coreServices.rootConfig, 216 | ldapAuth: ldapAuthExtensionPoint, 217 | }, 218 | async init({ config, ldapAuth }) { 219 | ldapAuth.set({ 220 | resolvers: { 221 | async checkUserExists( 222 | ldapAuthOptions, 223 | searchFunction 224 | ): Promise { 225 | const { username } = ldapAuthOptions; 226 | 227 | // Do you custom checks 228 | // .... 229 | 230 | return true; 231 | }, 232 | }, 233 | }); 234 | }, 235 | }); 236 | }, 237 | }); 238 | ``` 239 | 240 | ### Add the login form 241 | 242 | > More on this in the frontend plugin documentation [here](../ldap-auth/README.md) 243 | 244 | We need to replace the existing Backstage demo authentication page with our custom one! 245 | 246 | In the `App.tsx` file, change the `createApp` function adding a `components` with our custom `SignInPage`In the `App.tsx` file change the `createApp` function to provide use our custom `SignInPage` in the `components` key. 247 | 248 | **Note:** This components isn't only UI, it also brings all the token state management and HTTP API calls to the backstage auth routes we already configured in the backend part. 249 | 250 | > `packages/app/src/App.tsx` 251 | 252 | ```tsx 253 | import { LdapAuthFrontendPage } from '@immobiliarelabs/backstage-plugin-ldap-auth'; 254 | 255 | const app = createApp({ 256 | // ... 257 | components: { 258 | SignInPage: (props) => ( 259 | 260 | ), 261 | }, 262 | // ... 263 | }); 264 | ``` 265 | 266 | And you're ready to go! If you need more use cases, like having multiple processes and need a shared token store instead of in-memory look at the [example folders](https://github.com/immobiliare/backstage-plugin-ldap-auth/examples/) 267 | 268 | ## Powered Apps 269 | 270 | Backstage Plugin LDAP Auth was created by the amazing Node.js team at [ImmobiliareLabs](http://labs.immobiliare.it/), the Tech dept of [Immobiliare.it](https://www.immobiliare.it), the #1 real estate company in Italy. 271 | 272 | We are currently using Backstage Plugin LDAP Auth in our products as well as our internal toolings. 273 | 274 | **If you are using Backstage Plugin LDAP Auth in production [drop us a message](mailto:opensource@immobiliare.it)**. 275 | 276 | ## Support & Contribute 277 | 278 | Made with ❤️ by [ImmobiliareLabs](https://github.com/immobiliare) & [Contributors](https://github.com/immobiliare/backstage-plugin-ldap-auth/CONTRIBUTING.md#contributors) 279 | 280 | We'd love for you to contribute to Backstage Plugin LDAP Auth! 281 | If you have any questions on how to use Backstage Plugin LDAP Auth, bugs and enhancement please feel free to reach out by opening a [GitHub Issue](https://github.com/immobiliare/backstage-plugin-ldap-auth). 282 | 283 | ## License 284 | 285 | Backstage Plugin LDAP Auth is licensed under the MIT license. 286 | See the [LICENSE](https://github.com/immobiliare/backstage-plugin-ldap-auth/LICENSE) file for more information. 287 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [4.3.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.1-alpha.0...v4.3.1) (2024-10-18) 7 | 8 | **Note:** Version bump only for package root 9 | 10 | ## [4.3.1-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.0...v4.3.1-alpha.0) (2024-10-18) 11 | 12 | ### Bug Fixes 13 | 14 | - password printed in console ([e58c353](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/e58c3539a784909284f56b91740720f95b43f61f)) 15 | 16 | # [4.3.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.0-alpha.1...v4.3.0) (2024-07-23) 17 | 18 | **Note:** Version bump only for package root 19 | 20 | # [4.3.0-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.3.0-alpha.0...v4.3.0-alpha.1) (2024-07-23) 21 | 22 | ### Bug Fixes 23 | 24 | - **backstage:** pluginPackage and pluginId ([1284e95](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/1284e9577924652f9c6c688a62b4d59ff09f7512)) 25 | 26 | # [4.3.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.2.0...v4.3.0-alpha.0) (2024-07-23) 27 | 28 | ### Bug Fixes 29 | 30 | - Improve error message for no user details returned ([d28c7f4](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/d28c7f4606d9a85abf7a95bb339d3e019ed8f007)), closes [#700](https://github.com/immobiliare/backstage-plugin-ldap-auth/issues/700) 31 | - tests ([4d2863c](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/4d2863c1845c593a790803153c3cdd80639f473a)), closes [#700](https://github.com/immobiliare/backstage-plugin-ldap-auth/issues/700) 32 | 33 | ### Features 34 | 35 | - bump dependecies and remove deprecations ([918c021](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/918c02115a5f1c9e65b10a797a16775779420e90)) 36 | 37 | # [4.2.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.2.0-alpha.0...v4.2.0) (2024-03-26) 38 | 39 | **Note:** Version bump only for package root 40 | 41 | # [4.2.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.1.0...v4.2.0-alpha.0) (2024-03-25) 42 | 43 | ### Features 44 | 45 | - Support for Signin Error; Parameterizing Username label ([84941fa](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/84941fae0c1d42e82edda5872dfd37ca5970666e)) 46 | - upgrade auth backend ([e05c6b6](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/e05c6b605e060079dfb920289cc66ab4f55a3087)) 47 | 48 | # [4.1.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.1.0-alpha.1...v4.1.0) (2024-03-11) 49 | 50 | **Note:** Version bump only for package root 51 | 52 | # [4.1.0-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.1.0-alpha.0...v4.1.0-alpha.1) (2024-03-07) 53 | 54 | ### Features 55 | 56 | - refactored code API and added docs ([98b384c](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/98b384c407adb0a70a8b954181d1a8be3e63184c)) 57 | 58 | # [4.1.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.0.0...v4.1.0-alpha.0) (2024-03-05) 59 | 60 | ### Features 61 | 62 | - support for new backend system ([d711af4](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/d711af4ef7f5a95f9d53fbea8c5f67d0583fd0f3)) 63 | 64 | # [4.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v4.0.0-alpha.0...v4.0.0) (2023-09-22) 65 | 66 | **Note:** Version bump only for package root 67 | 68 | # [4.0.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.2-alpha.0...v4.0.0-alpha.0) (2023-09-22) 69 | 70 | ### Bug Fixes 71 | 72 | - removed parentheses being rendered after LoginForm component ([1604b78](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/1604b78d51e2e556fd8c513fbd62b85bc018b5aa)) 73 | 74 | ### Features 75 | 76 | - upgrade auth packages ([75f673e](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/75f673ee408cc51b610dc6dd11862a2731490372)) 77 | 78 | ### BREAKING CHANGES 79 | 80 | - This upgrade breaks the types with all Backstage versions minor of 1.18 81 | 82 | ## [3.0.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.1...v3.0.2-alpha.0) (2023-04-18) 83 | 84 | **Note:** Version bump only for package root 85 | 86 | ## [3.0.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.1-alpha.1...v3.0.1) (2023-03-10) 87 | 88 | **Note:** Version bump only for package root 89 | 90 | ## [3.0.1-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.1-alpha.0...v3.0.1-alpha.1) (2023-03-10) 91 | 92 | ### Bug Fixes 93 | 94 | - **backend:** authenticate function is undefined ([6a561d0](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/6a561d046b3bcba97ad84fd5a69a21c770bc55b9)) 95 | 96 | ## [3.0.1-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.0...v3.0.1-alpha.0) (2023-03-08) 97 | 98 | ### Bug Fixes 99 | 100 | - packages/ldap-auth/package.json to reduce vulnerabilities ([76be63c](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/76be63cefb2a0a5b7b11f9d62ba9d1e7b9004e2c)) 101 | - **prerelease:** npm prerelease bug ([00994c1](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/00994c101a76088023aa3df876a844530386fe23)) 102 | 103 | # [3.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v3.0.0-alpha.0...v3.0.0) (2023-01-16) 104 | 105 | **Note:** Version bump only for package root 106 | 107 | # [3.0.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.4...v3.0.0-alpha.0) (2023-01-13) 108 | 109 | ### Features 110 | 111 | - the children parameter type of LdapSignInPageProps type is now a ReactNode ([1d9632f](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/1d9632f55ce32888c9a5cc28659629582d126d99)) 112 | 113 | ### BREAKING CHANGES 114 | 115 | - the children parameter type of LdapSignInPageProps type is now a ReactNode 116 | 117 | ## [2.0.4](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.3...v2.0.4) (2022-12-07) 118 | 119 | ## [2.0.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1...v2.0.2-alpha.0) (2022-11-29) 120 | 121 | **Note:** Version bump only for package root 122 | 123 | ## [2.0.3](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.2...v2.0.3) (2022-12-07) 124 | 125 | ### Bug Fixes 126 | 127 | - default cookie field ([60cdd39](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/60cdd39e7b6de63757e106dd61090152900d9868)), closes [#142](https://github.com/immobiliare/backstage-plugin-ldap-auth/issues/142) 128 | - removes ?? for || ([2c30828](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/2c30828bb815c87f12b3a3471ff7bfa81454e878)) 129 | 130 | ## [2.0.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1...v2.0.2) (2022-11-29) 131 | 132 | ## [2.0.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1...v2.0.2-alpha.0) (2022-11-29) 133 | 134 | ### Bug Fixes 135 | 136 | - documentation typo ([ca575c0](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/ca575c0c1bc351c63a0f0c6d97695cc22ac968ed)) 137 | 138 | ## [2.0.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1-alpha.2...v2.0.1) (2022-11-23) 139 | 140 | **Note:** Version bump only for package root 141 | 142 | ## [2.0.1-alpha.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1-alpha.1...v2.0.1-alpha.2) (2022-11-23) 143 | 144 | **Note:** Version bump only for package root 145 | 146 | ## [2.0.1-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.1-alpha.0...v2.0.1-alpha.1) (2022-11-22) 147 | 148 | **Note:** Version bump only for package root 149 | 150 | ## [2.0.1-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.0...v2.0.1-alpha.0) (2022-11-22) 151 | 152 | **Note:** Version bump only for package root 153 | 154 | # [2.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.3...v2.0.0) (2022-11-22) 155 | 156 | ### Bug Fixes 157 | 158 | - **ldap.ts:** default auth function signature ([9b7c25a](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/9b7c25a73d5eff55dce78f892f63e33c243dcd90)) 159 | 160 | ### Features 161 | 162 | - reworked options and injectables ([3ecc9cb](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/3ecc9cb7cb111a5ac24981ab7eb8879809ad860a)) 163 | 164 | ### BREAKING CHANGES 165 | 166 | - Structure and options 167 | 168 | # [2.0.0-alpha.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2022-11-21) 169 | 170 | ### Bug Fixes 171 | 172 | - **ldap.ts:** default auth function signature ([3f2dd02](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/3f2dd0270dd1fd137d28a81101943d0014bf32bc)) 173 | 174 | # [2.0.0-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v2.0.0-alpha.0...v2.0.0-alpha.1) (2022-11-21) 175 | 176 | **Note:** Version bump only for package root 177 | 178 | # [2.0.0-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.2-alpha.0...v2.0.0-alpha.0) (2022-11-17) 179 | 180 | ### Bug Fixes 181 | 182 | - allow no children to the LdapAuthFrontendPage ([7624203](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/7624203260d6201815a1f8c79a4be98ba880cb89)) 183 | 184 | ### Features 185 | 186 | - reworked options and injectables ([b6b1445](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/b6b144539827301a7c95d6547d0217a1fe3cc103)) 187 | 188 | ### BREAKING CHANGES 189 | 190 | - Structure and options 191 | 192 | ## [1.1.2-alpha.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.1...v1.1.2-alpha.0) (2022-10-28) 193 | 194 | **Note:** Version bump only for package root 195 | 196 | ## [1.1.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.1.0...v1.1.1) (2022-10-03) 197 | 198 | ### Bug Fixes 199 | 200 | - increase token expire ms ([1f92d24](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/1f92d2449fed2d630ac375670834c70f8862243e)) 201 | 202 | # [1.1.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.4...v1.1.0) (2022-09-30) 203 | 204 | ### Features 205 | 206 | - added possibility to increase token expire time ([e7130d8](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/e7130d85bfa36c1ad17581e628bb57ab1dfd0f65)) 207 | - exports more utility functions ([3e00432](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/3e00432e1b11da04ff42f534a794fdce29a1db28)) 208 | 209 | ## [1.0.4](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.4-alpha.2...v1.0.4) (2022-09-28) 210 | 211 | **Note:** Version bump only for package root 212 | 213 | ## [1.0.4-alpha.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.4-alpha.1...v1.0.4-alpha.2) (2022-09-28) 214 | 215 | **Note:** Version bump only for package root 216 | 217 | ## [1.0.4-alpha.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.3...v1.0.4-alpha.1) (2022-09-28) 218 | 219 | ### Bug Fixes 220 | 221 | - packages/ldap-auth-backend/package.json to reduce vulnerabilities ([aab30fe](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/aab30fe30f5f2ba0269770b3476e64f7f299b60b)) 222 | 223 | ## [1.0.4-alpha.0](https://github.com/immobiliare/backstage-ldap-auth/compare/v1.0.3...v1.0.4-alpha.0) (2022-09-22) 224 | 225 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 226 | 227 | ## [1.0.3](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.2...v1.0.3) (2022-09-14) 228 | 229 | ### Bug Fixes 230 | 231 | - set cookie paired with rejectUnauthorized ([bc020ec](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/bc020ecd3bd10b1e952d4aa43a55a2ac00e4dea9)) 232 | 233 | ## [1.0.2](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.1...v1.0.2) (2022-09-05) 234 | 235 | ### Bug Fixes 236 | 237 | - package.json repository field ([6dc00dd](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/6dc00dd26eb1d81c3ddc9b354faa7fedd21b49aa)) 238 | 239 | ## [1.0.1](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.0...v1.0.1) (2022-09-05) 240 | 241 | ### Bug Fixes 242 | 243 | - readme screenshot url ([c3fab29](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/c3fab297f3fe8772d1fbed81ca136f0ed8246c14)) 244 | 245 | # [1.0.0](https://github.com/immobiliare/backstage-plugin-ldap-auth/compare/v1.0.0-alpha.1...v1.0.0) (2022-09-05) 246 | 247 | ### Features 248 | 249 | - added custom validate functions ([8994ec4](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/8994ec4cdee76a617b53e6c585367604ea1041a3)) 250 | - **frontend:** added helperText ([3060996](https://github.com/immobiliare/backstage-plugin-ldap-auth/commit/30609964082f8de9496eb75d9216b70da860aac0)) 251 | 252 | # [1.0.0-alpha.1](https://github.com/immobiliare/backstage-ldap-auth/compare/v1.0.0-alpha.0...v1.0.0-alpha.1) (2022-08-26) 253 | 254 | **Note:** Version bump only for package @immobiliarelabs/backstage-plugin-ldap-auth 255 | -------------------------------------------------------------------------------- /packages/ldap-auth-backend/src/provider.test.ts: -------------------------------------------------------------------------------- 1 | import { AuthResolverContext } from '@backstage/plugin-auth-node'; 2 | import { ProviderLdapAuthProvider } from './provider'; 3 | import { defaultCheckUserExists, defaultLDAPAuthentication } from './ldap'; 4 | import { defaultAuthHandler, defaultSigninResolver } from './auth'; 5 | import { COOKIE_FIELD_KEY, JWTTokenValidator } from './jwt'; 6 | import { AuthenticationOptions } from 'ldap-authentication'; 7 | import jwt from 'jsonwebtoken'; 8 | import { AuthenticationError } from '@backstage/errors'; 9 | import { AUTH_MISSING_CREDENTIALS, JWT_EXPIRED_TOKEN } from './errors'; 10 | import Keyv from 'keyv'; 11 | 12 | export function createProvider() { 13 | const sub = 'my-uid-name'; 14 | const token = jwt.sign({ sub }, 'secret', { 15 | expiresIn: '1min', 16 | }); 17 | const authHandler = jest.fn( 18 | defaultAuthHandler 19 | ) as typeof defaultAuthHandler; 20 | const checkUserExists = jest.fn(() => 21 | Promise.resolve(true) 22 | ) as typeof defaultCheckUserExists; 23 | const signInResolver = jest.fn( 24 | defaultSigninResolver 25 | ) as typeof defaultSigninResolver; 26 | const signInWithCatalogUser = jest.fn(async ({}) => ({ 27 | token, 28 | })); 29 | const findCatalogUser = jest.fn(async ({}) => ({ 30 | profile: { 31 | email: 'myemail@mail.com', 32 | displayName: 'John Doeh', 33 | }, 34 | })); 35 | const ldapAuthentication = jest.fn( 36 | async ( 37 | username: string, 38 | password: string, 39 | ldapAuthOptions: AuthenticationOptions 40 | ) => 41 | defaultLDAPAuthentication( 42 | username, 43 | password, 44 | ldapAuthOptions, 45 | (options: AuthenticationOptions) => 46 | Promise.resolve({ 47 | [options.usernameAttribute as string]: sub, 48 | }) 49 | ) 50 | ) as typeof defaultLDAPAuthentication; 51 | const provider = new ProviderLdapAuthProvider({ 52 | resolverContext: { 53 | signInWithCatalogUser, 54 | findCatalogUser, 55 | } as unknown as AuthResolverContext, 56 | authHandler, 57 | checkUserExists, 58 | signInResolver, 59 | ldapAuthentication, 60 | tokenValidator: new JWTTokenValidator(new Keyv()), 61 | cookies: { 62 | field: COOKIE_FIELD_KEY, 63 | secure: true, 64 | }, 65 | ldapAuthenticationOptions: { ldapOpts: { url: 'localhost' } }, 66 | }); 67 | 68 | return { 69 | provider, 70 | sub, 71 | token, 72 | authHandler, 73 | checkUserExists, 74 | signInResolver, 75 | ldapAuthentication, 76 | signInWithCatalogUser, 77 | findCatalogUser, 78 | }; 79 | } 80 | 81 | describe('LdapAuthProvider login tests', () => { 82 | it('Test refresh for login with username and password', async () => { 83 | const { 84 | provider, 85 | sub, 86 | token, 87 | ldapAuthentication, 88 | authHandler, 89 | checkUserExists, 90 | } = createProvider(); 91 | const reqMock = { 92 | body: { username: sub, password: 'hello-world' }, 93 | // [COOKIE_FIELD_KEY]: 'token-for-user:my-uid-name', 94 | method: 'POST', 95 | }; 96 | const resMock = { 97 | cookie: jest.fn(), 98 | json: jest.fn(), 99 | clearCookie: jest.fn(), 100 | }; 101 | await provider.refresh(reqMock as any, resMock as any); 102 | expect(resMock.json).toHaveBeenCalledWith({ 103 | backstageIdentity: { 104 | identity: { 105 | ownershipEntityRefs: [], 106 | type: 'user', 107 | userEntityRef: sub, 108 | }, 109 | token, 110 | }, 111 | profile: undefined, 112 | providerInfo: {}, 113 | }); 114 | expect(authHandler).toHaveBeenCalledTimes(1); 115 | expect(ldapAuthentication).toHaveBeenCalledTimes(1); 116 | expect(checkUserExists).not.toHaveBeenCalled(); 117 | expect(resMock.cookie).toHaveBeenCalled(); 118 | expect(resMock.clearCookie).not.toHaveBeenCalled(); 119 | }); 120 | 121 | it('Test refresh with token', async () => { 122 | const { 123 | provider, 124 | sub, 125 | token, 126 | ldapAuthentication, 127 | authHandler, 128 | checkUserExists, 129 | } = createProvider(); 130 | const oldToken = jwt.sign({ sub }, 'secret', { 131 | expiresIn: '1min', 132 | }); 133 | const reqMock = { 134 | body: {}, 135 | cookies: { [COOKIE_FIELD_KEY]: oldToken }, 136 | method: 'POST', 137 | }; 138 | const resMock = { 139 | cookie: jest.fn(), 140 | json: jest.fn(), 141 | clearCookie: jest.fn(), 142 | }; 143 | await provider.refresh(reqMock as any, resMock as any); 144 | expect(resMock.json).toHaveBeenCalledWith({ 145 | backstageIdentity: { 146 | identity: { 147 | ownershipEntityRefs: [], 148 | type: 'user', 149 | userEntityRef: sub, 150 | }, 151 | token, 152 | }, 153 | profile: undefined, 154 | providerInfo: {}, 155 | }); 156 | expect(authHandler).toHaveBeenCalledTimes(1); 157 | expect(ldapAuthentication).not.toHaveBeenCalled(); 158 | expect(checkUserExists).toHaveBeenCalledTimes(1); 159 | expect(resMock.cookie).toHaveBeenCalled(); 160 | expect(resMock.clearCookie).not.toHaveBeenCalled(); 161 | }); 162 | 163 | it('Test refresh should not accept GET', async () => { 164 | const { 165 | provider, 166 | sub, 167 | ldapAuthentication, 168 | authHandler, 169 | checkUserExists, 170 | } = createProvider(); 171 | const oldToken = jwt.sign({ sub }, 'secret', { 172 | expiresIn: '1min', 173 | }); 174 | const reqMock = { 175 | body: {}, 176 | cookies: { [COOKIE_FIELD_KEY]: oldToken }, 177 | method: 'GET', 178 | }; 179 | const resMock = { 180 | cookie: jest.fn(), 181 | json: jest.fn(), 182 | clearCookie: jest.fn(), 183 | }; 184 | await expect( 185 | provider.refresh(reqMock as any, resMock as any) 186 | ).rejects.toEqual(new AuthenticationError('Method not allowed')); 187 | expect(resMock.json).not.toHaveBeenCalled(); 188 | expect(authHandler).not.toHaveBeenCalled(); 189 | expect(ldapAuthentication).not.toHaveBeenCalled(); 190 | expect(checkUserExists).not.toHaveBeenCalled(); 191 | expect(resMock.cookie).not.toHaveBeenCalled(); 192 | expect(resMock.clearCookie).toHaveBeenCalledWith(COOKIE_FIELD_KEY); 193 | }); 194 | 195 | it('Test refresh should throw right error if missing credentials or token', async () => { 196 | const { provider, ldapAuthentication, authHandler, checkUserExists } = 197 | createProvider(); 198 | const reqMock = { 199 | body: {}, 200 | cookies: {}, 201 | method: 'POST', 202 | }; 203 | const resMock = { 204 | cookie: jest.fn(), 205 | json: jest.fn(), 206 | clearCookie: jest.fn(), 207 | }; 208 | await expect( 209 | provider.refresh(reqMock as any, resMock as any) 210 | ).rejects.toEqual(new AuthenticationError(AUTH_MISSING_CREDENTIALS)); 211 | expect(resMock.json).not.toHaveBeenCalled(); 212 | expect(authHandler).not.toHaveBeenCalled(); 213 | expect(ldapAuthentication).not.toHaveBeenCalled(); 214 | expect(checkUserExists).not.toHaveBeenCalled(); 215 | expect(resMock.cookie).not.toHaveBeenCalled(); 216 | expect(resMock.clearCookie).toHaveBeenCalledWith(COOKIE_FIELD_KEY); 217 | }); 218 | }); 219 | 220 | describe('LdapAuthProvider token invalidation tests', () => { 221 | afterEach(() => { 222 | jest.useRealTimers(); 223 | }); 224 | it('Test refresh the old token should be invalid', async () => { 225 | const timer = jest.useFakeTimers(); 226 | 227 | const oldToken = jwt.sign({ sub: 'my-uid-name' }, 'secret', { 228 | expiresIn: '1min', 229 | }); 230 | timer.advanceTimersByTime(2000); 231 | const { provider, sub, token } = createProvider(); 232 | 233 | const reqMock = { 234 | body: {}, 235 | cookies: { [COOKIE_FIELD_KEY]: oldToken }, 236 | method: 'POST', 237 | }; 238 | const resMock = { 239 | cookie: jest.fn(), 240 | json: jest.fn(), 241 | clearCookie: jest.fn(), 242 | }; 243 | await provider.refresh(reqMock as any, resMock as any); 244 | expect(resMock.json).toHaveBeenCalledWith({ 245 | backstageIdentity: { 246 | identity: { 247 | ownershipEntityRefs: [], 248 | type: 'user', 249 | userEntityRef: sub, 250 | }, 251 | token, 252 | }, 253 | profile: undefined, 254 | providerInfo: {}, 255 | }); 256 | // Not I redo the request with the old token 257 | timer.advanceTimersByTime(2000); 258 | expect( 259 | provider.refresh(reqMock as any, resMock as any) 260 | ).rejects.toEqual(new Error(JWT_EXPIRED_TOKEN)); 261 | }); 262 | 263 | it('Test refresh the new token should be valid', async () => { 264 | const timer = jest.useFakeTimers(); 265 | 266 | const oldToken = jwt.sign({ sub: 'my-uid-name' }, 'secret', { 267 | expiresIn: '1min', 268 | }); 269 | timer.advanceTimersByTime(2000); 270 | const { provider, sub, token, signInWithCatalogUser } = 271 | createProvider(); 272 | 273 | const reqMock = { 274 | body: {}, 275 | cookies: { [COOKIE_FIELD_KEY]: oldToken }, 276 | method: 'POST', 277 | }; 278 | const resMock = { 279 | cookie: jest.fn(), 280 | json: jest.fn(), 281 | clearCookie: jest.fn(), 282 | }; 283 | await provider.refresh(reqMock as any, resMock as any); 284 | expect(resMock.json).toHaveBeenCalledWith({ 285 | backstageIdentity: { 286 | identity: { 287 | ownershipEntityRefs: [], 288 | type: 'user', 289 | userEntityRef: sub, 290 | }, 291 | token, 292 | }, 293 | profile: undefined, 294 | providerInfo: {}, 295 | }); 296 | 297 | // Not I redo the request with the new token 298 | timer.advanceTimersByTime(2000); 299 | const newToken = jwt.sign({ sub: 'my-uid-name' }, 'secret', { 300 | expiresIn: '1min', 301 | }); 302 | signInWithCatalogUser.mockReturnValue( 303 | Promise.resolve({ 304 | token: newToken, 305 | }) 306 | ); 307 | reqMock.cookies[COOKIE_FIELD_KEY] = token; 308 | await expect( 309 | provider.refresh(reqMock as any, resMock as any) 310 | ).resolves.toEqual(undefined); 311 | expect(resMock.json).lastCalledWith({ 312 | backstageIdentity: { 313 | identity: { 314 | ownershipEntityRefs: [], 315 | type: 'user', 316 | userEntityRef: sub, 317 | }, 318 | token: newToken, 319 | }, 320 | profile: undefined, 321 | providerInfo: {}, 322 | }); 323 | }); 324 | }); 325 | 326 | describe('LdapAuthProvider token logout', () => { 327 | afterEach(() => { 328 | jest.useRealTimers(); 329 | }); 330 | it('Logout should works', async () => { 331 | const timer = jest.useFakeTimers(); 332 | 333 | const oldToken = jwt.sign({ sub: 'my-uid-name' }, 'secret', { 334 | expiresIn: '1min', 335 | }); 336 | timer.advanceTimersByTime(2000); 337 | 338 | const { provider, token } = createProvider(); 339 | const reqMock = { 340 | body: {}, 341 | cookies: { [COOKIE_FIELD_KEY]: oldToken }, 342 | method: 'POST', 343 | }; 344 | 345 | const resMock: any = { 346 | cookie: jest.fn(), 347 | status: jest.fn(() => resMock), 348 | json: jest.fn(), 349 | clearCookie: jest.fn(), 350 | end: jest.fn(), 351 | }; 352 | 353 | await provider.refresh(reqMock as any, resMock as any); 354 | // here we have that "token" is a valid token 355 | 356 | reqMock.cookies[COOKIE_FIELD_KEY] = token; 357 | timer.advanceTimersByTime(2000); 358 | await expect( 359 | provider.logout(reqMock as any, resMock as any) 360 | ).resolves.toEqual(undefined); 361 | 362 | expect(resMock.status).toHaveBeenCalledWith(200); 363 | expect(resMock.clearCookie).toHaveBeenCalledWith(COOKIE_FIELD_KEY); 364 | expect(resMock.end).toHaveBeenCalled(); 365 | timer.advanceTimersByTime(2000); 366 | expect( 367 | provider.refresh(reqMock as any, resMock as any) 368 | ).rejects.toEqual(new Error(JWT_EXPIRED_TOKEN)); 369 | }); 370 | }); 371 | --------------------------------------------------------------------------------