├── .dockerignore ├── .env ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── REFERENCES.md ├── SECURITY.md ├── backend.dockerfile ├── cache ├── bundled-ca.pem ├── ca.pem ├── certbot │ └── .gitkeep ├── database │ └── .gitkeep └── keys │ ├── .gitkeep │ └── ca │ └── .gitkeep ├── certbot.dockerfile ├── crowdin.yml ├── cypress.json ├── cypress ├── entrypoint.sh ├── flush_environment.cmd ├── integration │ ├── authenticator.js │ ├── guest.js │ ├── sso.js │ └── user.js ├── plugins │ └── index.js ├── support │ ├── commands.js │ └── index.js └── wait-for-it.sh ├── docker-compose.cypress.yml ├── docker-compose.dev.yml ├── docker-compose.yml ├── frontend+proxy.dockerfile ├── js-backend ├── .env ├── entrypoint.sh ├── flows │ ├── audit.js │ ├── authenticator-cert.js │ ├── authenticator.js │ ├── index.js │ ├── local-auth.js │ └── sso-flow.js ├── index.js ├── keys │ ├── ca │ │ ├── .gitkeep │ │ └── e-corp.ca.pem │ └── demo │ │ ├── client.p12 │ │ ├── server_cert.pem │ │ └── server_key.pem ├── package-lock.json ├── package.json ├── scripts │ ├── create-client.bash │ ├── setup.bash │ └── v3.ext ├── tests │ └── unit │ │ ├── _shared.js │ │ ├── flows │ │ ├── audit.spec.js │ │ ├── authenticator-cert.spec.js │ │ ├── authenticator.spec.js │ │ ├── local-auth.spec.js │ │ └── sso-flow.spec.js │ │ └── utils │ │ ├── audit.spec.js │ │ ├── jwt.spec.js │ │ ├── mail.spec.js │ │ ├── middleware.spec.js │ │ └── password.spec.js ├── tmp │ └── .gitkeep ├── utils │ ├── audit.js │ ├── index.js │ ├── jwt.js │ ├── mail.js │ ├── middleware.js │ ├── password.js │ └── user.js └── websites.json ├── nginx ├── certbot-entrypoint.sh ├── default.conf ├── docker-entrypoint.sh └── security.txt ├── owasp_sso.sql ├── package-lock.json ├── package.json └── vue-ui ├── .env ├── .env.production ├── .gitignore ├── locales ├── de.json └── en.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── AuditLog.vue │ └── AuthenticatorManage.vue ├── i18n.js ├── locales │ ├── af-ZA.json │ ├── ar-SA.json │ ├── ca-ES.json │ ├── cs-CZ.json │ ├── da-DK.json │ ├── de-DE.json │ ├── el-GR.json │ ├── en-US.json │ ├── es-ES.json │ ├── fi-FI.json │ ├── fr-FR.json │ ├── he-IL.json │ ├── hu-HU.json │ ├── it-IT.json │ ├── ja-JP.json │ ├── ko-KR.json │ ├── nl-NL.json │ ├── no-NO.json │ ├── pl-PL.json │ ├── pt-BR.json │ ├── pt-PT.json │ ├── ro-RO.json │ ├── ru-RU.json │ ├── sr-SP.json │ ├── sv-SE.json │ ├── tr-TR.json │ ├── uk-UA.json │ ├── vi-VN.json │ ├── zh-CN.json │ └── zh-TW.json ├── main.js ├── plugins │ └── vee-validate.js ├── router │ └── index.js └── views │ ├── about.vue │ ├── audit.vue │ ├── change-password.vue │ ├── flow-in.vue │ ├── login.vue │ ├── register.vue │ ├── reset-password.vue │ └── two-factor-auth.vue ├── tests └── unit │ ├── .eslintrc.js │ ├── components │ ├── auditlog.spec.js │ └── authenticatormanage.spec.js │ └── views │ ├── about.spec.js │ ├── audit.spec.js │ ├── change-password.spec.js │ ├── flow-in.spec.js │ ├── login.spec.js │ ├── register.spec.js │ ├── reset-password.spec.js │ └── two-factor-auth.spec.js └── vue.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist 3 | **/keys/*.pem 4 | **/keys/ca/*.pem 5 | **/demo -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DOMAIN= 2 | STAGING=true 3 | EMAIL= -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '24 20 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /js-backend/keys/server_cert.pem 2 | /js-backend/keys/server_key.pem 3 | node_modules 4 | dist 5 | /cache/certbot/* 6 | !/cache/certbot/.gitkeep 7 | /cache/database/* 8 | !/cache/database/.gitkeep 9 | /js-backend/keys/bundled-ca.pem 10 | /cypress/integration/examples 11 | /cache/keys/*.pem 12 | /cypress/videos 13 | /cypress/screenshots 14 | /cypress/fixtures 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 13 3 | stages: 4 | - lint 5 | - unit 6 | - e2e 7 | services: 8 | - docker 9 | jobs: 10 | include: 11 | - stage: lint 12 | install: 13 | - cd js-backend 14 | - npm install 15 | script: 16 | - npm run lint 17 | - stage: lint 18 | install: 19 | - cd vue-ui 20 | - npm install 21 | script: 22 | - npm run lint 23 | - stage: unit 24 | install: 25 | - cd vue-ui 26 | - npm install 27 | script: 28 | - npm run test:unit 29 | - stage: unit 30 | install: 31 | - cd js-backend 32 | - npm install 33 | script: 34 | - npm run test:unit 35 | - stage: e2e 36 | env: 37 | - DOMAIN=frontend.localhost 38 | script: 39 | - docker --version 40 | - docker-compose --version 41 | - chmod -R 775 . 42 | - npm run docker:e2e -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing [![GitHub contributors](https://img.shields.io/github/contributors/OWASP/SSO_Project.svg)](https://github.com/OWASP/SSO_Project/graphs/contributors) 2 | 3 | ![GitHub issues by-label](https://img.shields.io/github/issues/OWASP/SSO_Project/help%20wanted.svg) 4 | ![GitHub issues by-label](https://img.shields.io/github/issues/OWASP/SSO_Project/good%20first%20issue.svg) 5 | 6 | ## Code Contributions 7 | 8 | The minimum requirements for code contributions are: 9 | 10 | 1. The code _must_ be compliant with the lint settings within each components `package.json` file. 11 | You can check if your code is compliant by running `npm run lint`. 12 | To fix most issues automatically, you can use `npm run lint:fix`. 13 | 2. All new and changed code _should_ have a corresponding unit and/or 14 | integration test. 15 | 3. Bigger changes _must_ have a corresponding e2e test. 16 | 4. Linting, as well as all unit, integration and e2e tests _should_ pass 17 | locally before opening a Pull Request. 18 | 5. All commits to the library must follow the [Developer Certificate of Origin](https://developercertificate.org/). 19 | 20 | ## I18N Contributions 21 | 22 | All contributions for other languages are highly appreciated! 23 | You can find learn more about it on our crowd-sourced 24 | [translation project on Crowdin](https://crowdin.com/project/owasp-single-sign-on). -------------------------------------------------------------------------------- /REFERENCES.md: -------------------------------------------------------------------------------- 1 | # References 2 | 3 | Did you write a blog post, magazine article or do a podcast about or 4 | mentioning OWASP Single Sign-On? Add it to this file and open a PR! The same 5 | goes for conference or meetup talks, workshops or trainings you did 6 | where this project was mentioned or used! 7 | 8 | > :mega: marks short friendly shout outs. 9 | > :tv: marks presentations and info material. 10 | > :newspaper: marks journalistic reports. 11 | > :dollar: bill marks commercial resources. 12 | 13 | ## Awards :trophy: 14 | 15 | ## Web Links 16 | 17 | ### Pod- & Webcasts 18 | 19 | ### Blogs & Articles 20 | 21 | ## Lectures and Trainings 22 | 23 | ## Summits & Open Source Events 24 | 25 | ## Conference and Meetup Appearances 26 | 27 | #### 2020 -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the OWASP SSO 4 | project. 5 | 6 | * [Reporting a Bug](#reporting-a-bug) 7 | * [Disclosure Policy](#disclosure-policy) 8 | * [Comments on this Policy](#comments-on-this-policy) 9 | 10 | ## Reporting a Bug 11 | 12 | The OWASP SSO team and community take all security reports serious. 13 | Thank you for improving the security of this product. We appreciate your efforts and 14 | responsible disclosure and will make every effort to acknowledge your 15 | contributions - the product has a security credit 16 | 17 | Report security bugs by emailing the lead maintainer at https://mailhide.io/e/Wno7k. 18 | 19 | The lead maintainer will acknowledge your email within 48 hours, and will send a 20 | more detailed response within 48 hours indicating the next steps in handling 21 | your report. After the initial reply to your report, the security team will 22 | endeavor to keep you informed of the progress towards a fix and full 23 | announcement, and may ask for additional information or guidance. 24 | 25 | Report security bugs in third-party modules to the person or team maintaining 26 | the module. Missing security patches are appreciated, but will not result in 27 | public acknowledgement. 28 | 29 | ## Disclosure Policy 30 | 31 | When the security team receives a security bug report, they will assign it to a 32 | primary handler. This person will coordinate the fix and release process, 33 | involving the following steps: 34 | 35 | * Confirm the problem and determine the affected versions. 36 | * Audit code to find any potential similar problems. 37 | * Prepare fixes for all releases still under maintenance. These fixes will be 38 | released as fast as possible to npm. 39 | 40 | ## Comments on this Policy 41 | 42 | If you have suggestions on how this process could be improved please submit a 43 | pull request. 44 | -------------------------------------------------------------------------------- /backend.dockerfile: -------------------------------------------------------------------------------- 1 | # Build files 2 | FROM node:lts as build-stage 3 | WORKDIR /app 4 | 5 | # Packages first for caching 6 | COPY js-backend/package.json /app/package.json 7 | RUN npm install --only=prod 8 | 9 | # Now application 10 | COPY js-backend /app 11 | 12 | # Run JS from alpine image 13 | FROM node:lts-alpine as production-stage 14 | 15 | RUN apk update && apk add bash openssl && \ 16 | mkdir -p /app/keys && \ 17 | touch /app/keys/bundled-ca.pem 18 | WORKDIR /app 19 | COPY ./cypress/wait-for-it.sh /app/wait-for-it.sh 20 | COPY --from=build-stage /app /app 21 | RUN chmod -R 775 /app/*.sh && \ 22 | chmod -R 775 /app/scripts/*.bash 23 | 24 | EXPOSE 3000 25 | CMD ["/app/entrypoint.sh"] -------------------------------------------------------------------------------- /cache/bundled-ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGOTCCBCGgAwIBAgIUDZVyNBU8+PS18lKPzTgimlg/U3MwDQYJKoZIhvcNAQEL 3 | BQAwgasxEjAQBgNVBAMMCU9XQVNQIFNTTzEZMBcGA1UECgwQT1dBU1AgRXVyb3Bl 4 | IFZaVzEUMBIGA1UECwwLU1NPX1Byb2plY3QxMzAxBgkqhkiG9w0BCQEWJEphbWVz 5 | Q3VsbHVtQHVzZXJzLm5vcmVwbHkuZ2l0aHViLmNvbTEPMA0GA1UEBwwGQi05NjYw 6 | MREwDwYDVQQIDAhPcGJyYWtlbDELMAkGA1UEBhMCQkUwHhcNMjAwNDExMTM0NDA5 7 | WhcNMjUwNDEwMTM0NDA5WjCBqzESMBAGA1UEAwwJT1dBU1AgU1NPMRkwFwYDVQQK 8 | DBBPV0FTUCBFdXJvcGUgVlpXMRQwEgYDVQQLDAtTU09fUHJvamVjdDEzMDEGCSqG 9 | SIb3DQEJARYkSmFtZXNDdWxsdW1AdXNlcnMubm9yZXBseS5naXRodWIuY29tMQ8w 10 | DQYDVQQHDAZCLTk2NjAxETAPBgNVBAgMCE9wYnJha2VsMQswCQYDVQQGEwJCRTCC 11 | AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMQGMecB6rMMP4p74Fcn9Mqk 12 | KN8PhnoTXVZdSgGrplt7luJ3VqBM3XOb55yhrY279AUHcYCBDGby5Iw+CqdqSogE 13 | 7FwL9mSpA2/fxHlUnalBUbobzKKSUFx7AzteDs82cIgraZL4ZabDLU65VAargL/J 14 | W0uELCyJOeQY8rWGIMXQUQi2GSVSGssk18zzLGXmWcdOo/B5PxjrqGcwdLmqKRvV 15 | ba3Co8oc0G61UDAAhIvCY8RMK36kfD42kYRT57hS50ymxVpMSO7sdex/xhH5aLwt 16 | +J1a1fKfIIMQa8Wby57m9r1zfQjrwMU82j8smHPmkvdIeqNnGWN2qO2JzeJctwIi 17 | tDbn8WKURYlJKBTE2DJiej7Xj1n+GsAcfux2WU77I+8mF+7T/3tTQGBn3gk4cqay 18 | 9msCYtmhEjphqMo4xNY9EhoB+yPCRvk+QM+zDRslwsXiu0cElZ68YjA+67r7gCmJ 19 | WkNL9wubdNSixJTN05dgHKapaCdPPVzZP4edwOEijMjQDZhPnEzbp7KP7sA/BQ6n 20 | YMWZSkFBRvlFf5v7p2vE/as8LAjP6QSRMXBKu+qwZL2ZfCGFYcHGyTTR+YnxbkfV 21 | 3RBz2axFJV60PyvDgTfnPNEkYjHVtoctmRfgy41qEoc6ZvxJVtbaYXYZrLuluRdd 22 | 0Bz5UUm6ijjMnkRQY8QJAgMBAAGjUzBRMB0GA1UdDgQWBBROvaTOi4UnbJyqKCxx 23 | HDHHjIFLTDAfBgNVHSMEGDAWgBROvaTOi4UnbJyqKCxxHDHHjIFLTDAPBgNVHRMB 24 | Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQDDFkii9OKxMnErIiHKt/ZogFbQ 25 | ziC9ZVB4IKm/1uOVEffOd5Dy5sseS/k28u++EV7TJ4xuOH/RkS1+yi9YJ/OI3YMq 26 | m55TZCheoWXY4DH/BZR0XDchtbhBf28WsZTP7a6LbFzEF0oMj+q1TNOTdbBGtTbg 27 | z5om4+OFdmBbv1V/mIbogs9N8rUg2bVW7LK18TBOtw8dymavNWImFiKfE3PkkOLW 28 | gli9Ej1WLw7BFhD3Mzp3lGUa0d4XV0mOB1D/b+9Nl8VsUY/ATtnwYEh/yAzLvmnb 29 | ILlHcX/Con4wHAu+wHZlPesYHJ1eb3XSOUUygGaGXSGWtIG4VC5T+npJVdZwjEhk 30 | 6cRFCjMFi3oKZcFMhcXCy+Sl9V7PxC7jHNEDx7qin5qg6VwLON5re1gM8gwQfaOs 31 | nx25gawwFfvorjqvFWZvM61MPTzuA7Xw85qPR6wp36tk4aiP73G2XH6T47bz4k/d 32 | sL76KusJjjhLEuwVlJd0zMDm9r3eyp8xZ9dRi+UUCFndVjAjOonBb4tIQYf4O7Up 33 | rE7Nl/gqx6DRejZSBLj5z6J1zQ4f7qZawYKO+A/gUrUVFWpJKu9LtjmpBWi5A0Ui 34 | QhcRL4P5mmQwaQWUt6IOkhYZHBED9G0DGazm7VMtiRTfCxGesoCwLe6LYEy3slY1 35 | 0cP20QggiC9GGzpIyg== 36 | -----END CERTIFICATE----- 37 | -------------------------------------------------------------------------------- /cache/ca.pem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/cache/ca.pem -------------------------------------------------------------------------------- /cache/certbot/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/cache/certbot/.gitkeep -------------------------------------------------------------------------------- /cache/database/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/cache/database/.gitkeep -------------------------------------------------------------------------------- /cache/keys/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/cache/keys/.gitkeep -------------------------------------------------------------------------------- /cache/keys/ca/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/cache/keys/ca/.gitkeep -------------------------------------------------------------------------------- /certbot.dockerfile: -------------------------------------------------------------------------------- 1 | FROM certbot/certbot 2 | 3 | COPY nginx/certbot-entrypoint.sh /opt/certbot/custom-entrypoint.sh 4 | RUN chmod 777 /opt/certbot/custom-entrypoint.sh 5 | ENTRYPOINT ["/opt/certbot/custom-entrypoint.sh"] -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /vue-ui/src/locales/en-US.json 3 | translation: /vue-ui/src/locales/%locale%.json 4 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "css7c8", 3 | "defaultCommandTimeout": 10000, 4 | "env": { 5 | "basePassword": "OWASPSSOPass123", 6 | "basePasswordHash": "$argon2id$v=19$m=200000,t=5,p=1$qFDmryXb3T8IGL08FpXzeg$28HdKkGqkKlm+dvN88xQ+CyJaYtcyeg8x+Glt6dXsAU", 7 | "emailAddress": "jamescullum@owasp.org", 8 | "logLogCount": 2 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /cypress/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Installing dependancies" 3 | npm install 4 | 5 | echo "Waiting on application to be ready" 6 | /cypress/wait-for-it.sh frontend.localhost:443 -t 600 7 | 8 | echo "Starting test" 9 | if [ -z "$CYPRESS_RECORD_KEY" ]; then 10 | npm run cy:run 11 | else 12 | npm run cy:run -- --record --key $CYPRESS_RECORD_KEY 13 | fi -------------------------------------------------------------------------------- /cypress/flush_environment.cmd: -------------------------------------------------------------------------------- 1 | rem Delete environment variables for local development 2 | set DBDATABASE= 3 | set DBHOST= 4 | set DBPASS= 5 | set DBUSER= 6 | SET SMTPHOST= 7 | set SMTPPASS= 8 | set SMTPPORT= 9 | set SMTPSECURE= 10 | set SMTPUSER= -------------------------------------------------------------------------------- /cypress/integration/guest.js: -------------------------------------------------------------------------------- 1 | describe("Guest Activity", () => { 2 | beforeEach(() => { 3 | cy.clearData(); 4 | cy.clearLocalStorage(); 5 | }); 6 | 7 | it("Registers a new account", () => { 8 | cy.visit("/"); 9 | cy.get("#goRegister").click(); 10 | 11 | cy.get("#email").type(Cypress.env("emailAddress")); 12 | cy.get("#agree").check({force: true}); 13 | cy.get("#termsLink").should("have.attr", "href", "https://owasp.org/www-policy/operational/general-disclaimer"); 14 | cy.get("button[type=submit]").click(); 15 | cy.get(".alert-success").should("be.visible"); 16 | 17 | cy.get("#goLogin").click(); 18 | 19 | cy.getLastEmailBody().then(email => { 20 | expect(email.link.endpoint).to.equal("register"); 21 | cy.visit(email.link.path); 22 | 23 | cy.get("#password").type(Cypress.env("basePassword")); 24 | cy.get("#confirm").type(Cypress.env("basePassword")); 25 | cy.get("button[type=submit]").click(); 26 | 27 | cy.isAuthenticated(); 28 | }); 29 | }); 30 | 31 | it("Can reset/change password", () => { 32 | cy.registerUser(); 33 | 34 | cy.visit("/"); 35 | cy.get("#goReset").click(); 36 | cy.wait(50); 37 | cy.get("#email").type(Cypress.env("emailAddress")); 38 | cy.get("button[type=submit]").click(); 39 | 40 | cy.get(".alert-success").should("be.visible"); 41 | 42 | cy.getLastEmailBody().then(email => { 43 | expect(email.link.endpoint).to.equal("change-password"); 44 | cy.visit(email.link.path); 45 | 46 | const altPass = Cypress.env("basePassword") + "4"; 47 | cy.get("#password").type(altPass); 48 | cy.get("#confirm").type(altPass); 49 | cy.get("button[type=submit]").click(); 50 | 51 | cy.isAuthenticated(); 52 | }); 53 | }); 54 | 55 | it("Shows information page", () => { 56 | cy.visit("/"); 57 | 58 | cy.get("[href='#/about']").click(); 59 | cy.get("#title-security").click(); 60 | 61 | cy.get(".about-banner").should("be.visible"); 62 | 63 | cy.get(".brand img").click(); 64 | cy.isGuest(); 65 | }); 66 | 67 | it("Loads default page", () => { 68 | cy.visit("/"); 69 | 70 | cy.get("a[target='_privacy']").should("have.attr", "href", "https://owasp.org/www-policy/operational/privacy"); 71 | cy.get("a[target='_imprint']").should("have.attr", "href", "https://owasp.org/contact/"); 72 | cy.get(".brand img").should("have.attr", "src", "https://owasp.org/assets/images/logo.png"); 73 | cy.get(".footer p").should(el => expect(el.text().trim()).to.equal("OWASP Foundation")); 74 | cy.get(".footer").should("have.css", "color"); 75 | cy.get("body").should("have.css", "background-color"); 76 | }); 77 | }); -------------------------------------------------------------------------------- /cypress/integration/sso.js: -------------------------------------------------------------------------------- 1 | describe("SSO Flow", () => { 2 | beforeEach(() => { 3 | cy.clearData(); 4 | cy.clearLocalStorage(); 5 | }); 6 | 7 | it("Can do JWT flow", () => { 8 | cy.visit("/in/1"); 9 | cy.authenticate(); 10 | checkBranding(); 11 | 12 | cy.get("#ssoFlowOutForm").should("have.attr", "action", "https://postman-echo.com/post"); 13 | cy.get("#ssoFlowOutForm input[name=token]").invoke("val").then(value => { 14 | expect(value).to.have.string("."); 15 | }); 16 | }); 17 | 18 | it("Loads branding from SAML flow", () => { 19 | cy.visit("/in/saml?SAMLRequest=fZJZc6owGIb%2FCpN7EMEFMqKDuC8VhVr1xokhsggJJcGlv75WjzM9N71M8n55knmfVueapdKZFDxm1AJVRQUSoZgFMQ0t8O4PZAN02i2OsjSHdikiuiKfJeFCus9RDh8HFigLChniMYcUZYRDgaFnz2dQU1SYF0wwzFIg2ZyTQtxBDqO8zEjhkeIcY%2FK%2BmlkgEiLnsFLJGRcZojLBEVMwyx4bnR8OkHp3cEyReLz1NRDGIioPj%2Bjiw%2Fbciuct9m7BEoIFkMY9C%2By1t%2FpwmGs0G%2FvO4IBvQ2%2BaoKWO4sb0amL70ou6w001tCe3s1OkkzPrL4OkH%2Fr8iJJwbmbNRLkoY3PwVQ2CdbIz0%2BnOiTDPRW6erien%2FMSXA3kjSjq5rZd514g9VEOXwZYnpl0vmqv%2BoT7JR6Om1m0cSViOBgO3b8zRSS9rkdefpubeyFi8mQV2vfmxjr4uyNhsr7SW9Xjk5frmoLr93XTRG83FNV1v0A0Pt%2BU8dDR1mB8d%2B%2F5LzksyplwgKiygqVVdVmuyZviaBms6rGmKbjR2QHL%2FddGN6bPhv4o7PEMcjnzfld2F5wNp%2FTLlHgBPL%2BADXvwS4u9r0csC0H5ViO5iqXJAzjIJlOymcJQSfmQFJj%2B1tiq%2FMO3n6n8Z298%3D&RelayState=hello-relay"); 20 | cy.authenticate(); 21 | checkBranding(); 22 | 23 | cy.get("#ssoFlowOutForm").should("have.attr", "action", "https://postman-echo.com/post?saml"); 24 | cy.get("#ssoFlowOutForm input[name=SAMLResponse]").invoke("val").then(value => { 25 | expect(value).to.have.string("="); 26 | }); 27 | cy.get("#ssoFlowOutForm input[name=RelayState]").should("have.value", "hello-relay"); 28 | }); 29 | }); 30 | 31 | function checkBranding() { 32 | cy.get("a[target='_privacy']").should("have.attr", "href", "https://www.nbcuniversal.com/privacy"); 33 | cy.get("a[target='_imprint']").should("not.exist"); 34 | cy.get(".brand img").should("have.attr", "src", "https://www.e-corp-usa.com/images/e-corp-logo-blue.png"); 35 | cy.get(".footer p").should(el => expect(el.text().trim()).to.equal("E Corp")); 36 | cy.get("#flowLeaveButton").should("be.visible").should("contain.text", "E Corp").click(); 37 | } -------------------------------------------------------------------------------- /cypress/integration/user.js: -------------------------------------------------------------------------------- 1 | describe("User Activity", () => { 2 | beforeEach(() => { 3 | cy.clearData(); 4 | cy.authenticate(); 5 | }); 6 | 7 | it("Can log out", () => { 8 | cy.logout(); 9 | }); 10 | 11 | it("Can load more audit logs", () => { 12 | const insertValues = []; 13 | for(let i=0;i<10;i++) { 14 | insertValues.push("('8.8.8.8', 'US', 'login', 'password', 1)"); 15 | } 16 | cy.task("sql", "INSERT INTO audit (ip, country, object, action, user) VALUES "+(insertValues.join(",\n"))+";"); 17 | 18 | cy.get(".data-logs .accordion-row").should("have.length", 5); 19 | cy.get("#loadMoreAudit").click(); 20 | cy.get(".data-logs .accordion-row").should("have.length", 10); 21 | 22 | cy.get(".data-logs .accordion-row:last").click(); 23 | cy.get(".audit-logs").scrollTo("bottom"); 24 | cy.get(".data-logs .accordion-row:last .collapse.show .card-body").should("be.visible"); 25 | cy.get(".flag.flag-us").should("be.visible"); 26 | }); 27 | 28 | it("can report suspicious events", () => { 29 | cy.expectLogIncrease(0); 30 | 31 | cy.get(".data-logs .accordion-row:first").click(); 32 | cy.get(".data-logs .accordion-row:first .report-log").click(); 33 | 34 | cy.expectLogIncrease(2); 35 | }); 36 | 37 | it("Can log out other sessions", () => { 38 | cy.task("sql", "INSERT INTO userSessions (userId, token) VALUES (1, 'other-token')"); 39 | cy.task("sql", "SELECT * FROM userSessions WHERE userId = 1").should("have.length", 2); 40 | 41 | cy.expectLogIncrease(0); 42 | cy.get("#closeSessions").click(); 43 | 44 | cy.expectLogIncrease(1); 45 | cy.task("sql", "SELECT * FROM userSessions WHERE userId = 1").should("have.length", 1); 46 | }); 47 | 48 | it("Can log into two accounts and resume a session", () => { 49 | cy.visit("/"); 50 | cy.get("#resume-session a").should("have.length", 1); 51 | 52 | Cypress.env("emailAddress", "2_" + Cypress.env("emailAddress")); 53 | cy.authenticate(); 54 | 55 | cy.visit("/"); 56 | cy.get("#resume-session a").should("have.length", 2); 57 | 58 | cy.get("#resume-session a:first").click(); 59 | cy.isAuthenticated(); 60 | }); 61 | }); 62 | 63 | function expectLogs(num) { 64 | cy.get(".data-logs").find(".accordion-row").should("have.length", num); 65 | } -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // Get all emails 2 | Cypress.Commands.add("getEmails", () => { 3 | cy.request("http://"+Cypress.env("MAILHOST")+":8025/api/v2/messages").its("body"); 4 | }); 5 | 6 | // Just get the last email 7 | Cypress.Commands.add("getLastEmail", () => { 8 | return cy.getEmails().then(emails => { 9 | let result = null; 10 | if(emails.count > 0) { 11 | const sortedItems = emails.items.slice().sort((a, b) => b.Created - a.Created); 12 | result = sortedItems[0]; 13 | } 14 | return cy.wrap(result); 15 | }); 16 | }); 17 | 18 | // Only get the relevant parts from the last email, mostly used 19 | Cypress.Commands.add("getLastEmailBody", () => { 20 | return cy.getLastEmail().then(email => { 21 | const body = email.Content.Body.replace(/=\s+/g, ""); 22 | const linkParts = body.match(/(https?:\/\/([^\/]+?)\/#(\/([^\/]+?)(?:(?:\/)([^\/]+?))?))\s*----/); 23 | const link = { 24 | full: linkParts[1], 25 | host: linkParts[2], 26 | path: linkParts[3], 27 | endpoint: linkParts[4], 28 | }; 29 | if(linkParts.length > 4) { 30 | link.token = linkParts[5]; 31 | } 32 | 33 | return cy.wrap({ 34 | body, 35 | link, 36 | }); 37 | }); 38 | }); 39 | 40 | // Check if user is logged in 41 | Cypress.Commands.add("isAuthenticated", () => { 42 | cy.url().should("include", "/audit"); 43 | }); 44 | 45 | // Check if user is at landing page (= login) 46 | Cypress.Commands.add("isGuest", () => { 47 | cy.url().should("match", /#\/$/); 48 | }); 49 | 50 | // Expect number of audit logs 51 | Cypress.Commands.add("expectLogIncrease", num => { 52 | cy.get(".data-logs .accordion-row").should("have.length", Cypress.env("logLogCount") + num); 53 | }); 54 | 55 | // Shortcut to log in (-> showing 2fa screen) 56 | Cypress.Commands.add("login", () => { 57 | cy.registerUser(); // If we need a login, we will need the user as well 58 | cy.visit("/"); 59 | 60 | cy.get("#email").type(Cypress.env("emailAddress")); 61 | cy.get("#password").type(Cypress.env("basePassword")); 62 | cy.get(".btn").click(); 63 | }); 64 | 65 | // Shortcut to authenticate (login+2fa) 66 | Cypress.Commands.add("authenticate", () => { 67 | cy.login(); 68 | 69 | cy.get("#confirmEmail").click(); 70 | cy.get(".alert-success").should("be.visible"); 71 | 72 | cy.wait(1000); 73 | cy.getLastEmailBody().then(email => { 74 | expect(email.link.endpoint).to.equal("two-factor"); 75 | cy.visit(email.link.path); 76 | cy.isAuthenticated(); 77 | 78 | cy.wrap(true); 79 | }); 80 | }); 81 | 82 | // Assumes you"re on /audit 83 | Cypress.Commands.add("logout", () => { 84 | cy.get("#logout").click(); 85 | cy.isGuest(); 86 | }); 87 | 88 | // Reset database to empty 89 | Cypress.Commands.add("clearData", () => { 90 | cy.task("sqlBulk", [ 91 | "SET FOREIGN_KEY_CHECKS = 0", 92 | "TRUNCATE audit", 93 | "TRUNCATE authenticators", 94 | "TRUNCATE emailConfirm", 95 | "TRUNCATE passwords", 96 | "TRUNCATE userSessions", 97 | "TRUNCATE websites", 98 | "TRUNCATE users", 99 | "SET FOREIGN_KEY_CHECKS = 1", 100 | ]); 101 | }); 102 | 103 | // Add basic user to not have to register all the time again 104 | // Ignores if the user already exists 105 | Cypress.Commands.add("registerUser", () => { 106 | cy.task("sqlBulk", [["INSERT IGNORE INTO users (username, created, last_login) VALUES (?, NOW(), NULL)", Cypress.env("emailAddress")]]).then(result => { 107 | cy.task("sqlBulk", [[ 108 | "INSERT IGNORE INTO passwords (userId, password, created) VALUES (?, ?, NOW())", [ 109 | result[0].insertId, Cypress.env("basePasswordHash"), 110 | ] 111 | ]]); 112 | }); 113 | }); -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /docker-compose.cypress.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | # Used for e2e testing only 4 | services: 5 | e2e: 6 | image: cypress/browsers:node13.8.0-chrome81-ff75 7 | entrypoint: ["/cypress/entrypoint.sh"] 8 | volumes: 9 | - ./cypress:/cypress 10 | - ./cypress.json:/cypress.json 11 | - ./package.json:/package.json 12 | environment: 13 | - CYPRESS_RECORD_KEY 14 | - DBHOST=database 15 | - FRONTENDHOST=frontend.localhost 16 | - SMTPHOST=smtp 17 | - COMMIT_INFO_BRANCH=$TRAVIS_BRANCH 18 | - COMMIT_INFO_MESSAGE=$TRAVIS_COMMIT_MESSAGE 19 | - COMMIT_INFO_SHA=$TRAVIS_COMMIT -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | # Emulate database 5 | database: 6 | image: mysql:5 7 | volumes: 8 | - ./owasp_sso.sql:/docker-entrypoint-initdb.d/setup.sql 9 | - ./cache/database:/var/lib/mysql 10 | restart: always 11 | environment: 12 | MYSQL_ROOT_PASSWORD: insecure-default-root-password 13 | MYSQL_DATABASE: owasp_sso 14 | MYSQL_USER: owasp_sso 15 | MYSQL_PASSWORD: insecure-default-password 16 | ports: 17 | - 3306:3306 18 | 19 | # Emulate SMTP 20 | smtp: 21 | image: mailhog/mailhog 22 | restart: always 23 | ports: 24 | - 8025:8025 25 | 26 | # Emulate syslog server for the central SIEM 27 | # Run it alone like this: docker run --name rsyslog.service -h test-host -p 514:514 jumanjiman/rsyslog 28 | # You can test payloads like here: https://superuser.com/a/1229424/497745 29 | # It is not enabled by default, as the website does not fully work if the server does not exist. 30 | # You need to enable this manually in to websites.json 31 | #default-siem: 32 | # image: jumanjiman/rsyslog 33 | # hostname: default-siem 34 | # restart: always -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | backend: 5 | restart: always 6 | build: 7 | context: . 8 | dockerfile: backend.dockerfile 9 | volumes: 10 | - ./cache/keys:/app/keys 11 | - ./cache/bundled-ca.pem:/app/keys/bundled-ca.pem 12 | environment: 13 | - DOMAIN 14 | - FRONTENDPORT=443 15 | - DBHOST 16 | - DBUSER 17 | - DBDATABASE 18 | - DBPASS 19 | - SMTPHOST 20 | - SMTPPORT 21 | - SMTPSECURE 22 | - SMTPUSER 23 | - SMTPPASS 24 | - ISSUERNAME 25 | - FIDO2FACTOR 26 | - FIDO2TIMEOUT 27 | - PWNEDPASSFAILSAFE 28 | - PWHISTORY 29 | - ARGON2TYPE 30 | - ARGON2PARALLEL 31 | - ARGON2TIME 32 | - ARGON2MEMORY 33 | - AUDITPAGELENGTH 34 | - SYSLOGHEARTBEAT 35 | - SMTPFROM 36 | 37 | frontend.localhost: 38 | depends_on: 39 | - backend # Nginx fails if backend is not available 40 | restart: always 41 | build: 42 | context: . 43 | dockerfile: frontend+proxy.dockerfile 44 | ports: 45 | - mode: host 46 | protocol: tcp 47 | published: 80 48 | target: 80 49 | - mode: host 50 | protocol: tcp 51 | published: 443 52 | target: 443 53 | volumes: 54 | - ./cache/certbot/conf:/etc/letsencrypt 55 | - ./cache/certbot/www:/var/www/certbot 56 | - ./cache/bundled-ca.pem:/etc/nginx/crypto/ca.pem 57 | environment: 58 | - DOMAIN 59 | - TRUSTEDPROXYIP 60 | 61 | certbot: 62 | depends_on: 63 | - frontend.localhost 64 | build: 65 | context: . 66 | dockerfile: certbot.dockerfile 67 | restart: always 68 | volumes: 69 | - ./cache/certbot/conf:/etc/letsencrypt 70 | - ./cache/certbot/www:/var/www/certbot 71 | environment: 72 | - DOMAIN 73 | - STAGING 74 | - EMAIL 75 | 76 | -------------------------------------------------------------------------------- /frontend+proxy.dockerfile: -------------------------------------------------------------------------------- 1 | # Build files 2 | FROM node:lts as build-stage 3 | WORKDIR /app 4 | 5 | # Packages first for caching 6 | COPY vue-ui/package.json /app/package.json 7 | RUN npm install 8 | 9 | # Now application 10 | COPY vue-ui /app 11 | RUN npm run build 12 | 13 | # Prepare crypto components 14 | RUN mkdir -p /crypto && \ 15 | curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > /crypto/options-ssl-nginx.conf && \ 16 | curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > /crypto/ssl-dhparams.pem && \ 17 | openssl req -x509 -nodes -newkey rsa:1024 -days 7 -keyout '/crypto/snake-privkey.pem' -out '/crypto/snake-fullchain.pem' -subj '/CN=localhost' && \ 18 | touch /crypto/ca.pem 19 | 20 | # Run files from webserver 21 | FROM nginx:stable-alpine as production-stage 22 | RUN mkdir -p /etc/letsencrypt/live 23 | 24 | COPY nginx/default.conf /etc/nginx/conf.d/default.template 25 | COPY --from=build-stage /crypto /etc/nginx/crypto 26 | 27 | COPY nginx/docker-entrypoint.sh /etc/nginx/docker-entrypoint.sh 28 | RUN chmod 777 /etc/nginx/docker-entrypoint.sh 29 | COPY nginx/security.txt /app/owasp_sso/.well-known/security.txt 30 | COPY --from=build-stage /app/dist /app/owasp_sso 31 | 32 | CMD ["/etc/nginx/docker-entrypoint.sh"] -------------------------------------------------------------------------------- /js-backend/.env: -------------------------------------------------------------------------------- 1 | # Mandatory 2 | DOMAIN=localhost 3 | 4 | DBHOST=database 5 | DBUSER=owasp_sso 6 | DBDATABASE=owasp_sso 7 | DBPASS=insecure-default-password 8 | 9 | SMTPHOST=smtp 10 | SMTPPORT=1025 11 | SMTPSECURE=0 12 | SMTPUSER= 13 | SMTPPASS= 14 | 15 | # Optional 16 | ISSUERNAME=OWASP SSO 17 | FIDO2FACTOR=either 18 | FIDO2TIMEOUT=900 19 | 20 | BACKENDPORT=3000 21 | FRONTENDPORT=8080 22 | 23 | PWNEDPASSFAILSAFE=0 24 | PWHISTORY=3 25 | 26 | ARGON2TYPE=2 27 | ARGON2PARALLEL=1 28 | ARGON2TIME=5 29 | ARGON2MEMORY=200000 30 | 31 | AUDITPAGELENGTH=5 32 | SYSLOGHEARTBEAT=60 33 | 34 | SMTPFROM=SSO@owasp.org -------------------------------------------------------------------------------- /js-backend/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Checking environment" 4 | CHECKIP=$(getent hosts database) 5 | if [ -z "$CHECKIP" ]; then 6 | echo "Production environment detected, not waiting on database" 7 | else 8 | echo "Dev environment detected, waiting on database" 9 | /app/wait-for-it.sh database:3306 -t 600 10 | fi 11 | 12 | echo "Starting backend" 13 | npm run serve -------------------------------------------------------------------------------- /js-backend/flows/audit.js: -------------------------------------------------------------------------------- 1 | const {User, Audit} = require("../utils"); 2 | 3 | class AuditFlow { 4 | getMe(req, res) { 5 | User.findUserById(req.user.id).then(userData => { 6 | const {password, last_login, created, ...publicAttributes} = userData; 7 | publicAttributes.authenticators.forEach(v => { 8 | delete v.userCounter; 9 | delete v.userKey; 10 | }); 11 | const token = req.user.token; 12 | 13 | if(token) { 14 | User.validateSession(token).then(() => { 15 | publicAttributes.isAuthenticated = true; 16 | res.json(publicAttributes); 17 | }); 18 | } else { 19 | publicAttributes.isAuthenticated = false; 20 | res.json(publicAttributes); 21 | } 22 | }); 23 | } 24 | 25 | getAuditLogs(req, res) { 26 | const currentPage = parseInt(req.query.page) || 0; 27 | const pageSize = parseInt(process.env.AUDITPAGELENGTH) || 5; 28 | 29 | Audit.getList(req.user.id, currentPage*pageSize, pageSize).then(results => { 30 | res.json(results); 31 | }).catch(err => { 32 | res.status(500).send(err); 33 | }); 34 | } 35 | 36 | reportAuditLog(req, res, next) { 37 | const auditId = parseInt(req.body.id); 38 | if(!auditId) { 39 | return res.status(400).send("No ID provided"); 40 | } 41 | 42 | Audit.get(auditId).then(rows => { 43 | if(!rows.length || rows[0].user != req.user.id) { 44 | return res.status(404).send("Audit ID does not exist"); // to make sure you can not enumerate them 45 | } 46 | 47 | Audit.add(req, "session", "report", req.body.id).then(() => { 48 | // No need to close all sessions here, as we can just trigger a click to close other sessions in the frontend 49 | next(); 50 | }); 51 | }).catch(err => { 52 | return res.status(404).send("Audit ID does not exist"); 53 | }); 54 | } 55 | } 56 | 57 | exports.auditFlow = AuditFlow; -------------------------------------------------------------------------------- /js-backend/keys/ca/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/js-backend/keys/ca/.gitkeep -------------------------------------------------------------------------------- /js-backend/keys/ca/e-corp.ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGTzCCBDegAwIBAgIUJpEyBkm0fJJ+7i85bpm/UU1ZP4EwDQYJKoZIhvcNAQEL 3 | BQAwgbYxHTAbBgNVBAMMFE9XQVNQIFNpbmdsZSBTaWduLU9uMRkwFwYDVQQKDBBP 4 | V0FTUCBFdXJvcGUgVlpXMRQwEgYDVQQLDAtTU09fUHJvamVjdDEzMDEGCSqGSIb3 5 | DQEJARYkSmFtZXNDdWxsdW1AdXNlcnMubm9yZXBseS5naXRodWIuY29tMQ8wDQYD 6 | VQQHDAZCLTk2NjAxETAPBgNVBAgMCE9wYnJha2VsMQswCQYDVQQGEwJCRTAeFw0y 7 | MDAzMjExMzM2MTBaFw0yNTAzMjAxMzM2MTBaMIG2MR0wGwYDVQQDDBRPV0FTUCBT 8 | aW5nbGUgU2lnbi1PbjEZMBcGA1UECgwQT1dBU1AgRXVyb3BlIFZaVzEUMBIGA1UE 9 | CwwLU1NPX1Byb2plY3QxMzAxBgkqhkiG9w0BCQEWJEphbWVzQ3VsbHVtQHVzZXJz 10 | Lm5vcmVwbHkuZ2l0aHViLmNvbTEPMA0GA1UEBwwGQi05NjYwMREwDwYDVQQIDAhP 11 | cGJyYWtlbDELMAkGA1UEBhMCQkUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 12 | AoICAQDhyu6Rcmd6jez5UDdn0dxpq+bsCwsmycqpjzE3qJSF+Nji8oE5TXYNNEbQ 13 | mg+cDyYUCL76VAJnxqnN2//dsqKpj1bXTtqJqe5NyLAfAz45qIqCDbd4OQCPfFPl 14 | TFI/q/zYf6ge93BqFGq4JLt43TMTeqUnvGrfckaBpSA0R3GaXpwCwvryGEx/iwLz 15 | EVSUDRFloxcNbmWTGxgqyqnEM+Ptp0FL/6hPHm6E+t7GKtuKA+z68ud16RunqCRz 16 | oVfRr4flrm0iB6gIe1NiUjsmJuFTZM7gH+o8oo59zIeLltIvlpDUmpdHkPGxEn4n 17 | oRlgiwnuRDl6qdl0SoAxHjLfnjhdmgXW6fH/c6gtciQ3s+QAKidw/dfpC1xSQIdp 18 | ByOL/OHpTzkkWD9Q8I4lxo0Rf9GdRvB9oOPA121fWKPHMzDGHQk3JTNN5Aj+u27s 19 | QW5GS8J0LnIFclpgx3yeCVAmoSViGMn15jyr/0M+wHaXGNwA5GfnHlJTI+G4CBaq 20 | y2bdoM6y/vo9PRRQpaNVvvz/Slch8zeQBVRAHnwdKXXehWC/zkP3QOctxtMkbTJ4 21 | fLPopCS+EGAa2kbForycnUDMWxwv2R/omg0ZmhaD483aGMuzmiaDsFf8mNxD1+Or 22 | eQD05ZkzH7Gvi1+z8EIVNdzCJoO7rjVY42KVWBBo2eykRoYl0QIDAQABo1MwUTAd 23 | BgNVHQ4EFgQU/Hz/m30+tORkRfoBujT+euGrdX8wHwYDVR0jBBgwFoAU/Hz/m30+ 24 | tORkRfoBujT+euGrdX8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC 25 | AgEAcMIKlI0hnnxnGSYgbZ3Xc88H3S3jxWCuCQOnnQvl9kPvYxe3CDgGM8x7e+GO 26 | odWtjmhoKtbGNidyfbCCvNJ0R6OLKYLqDQTdq/KMy2jPLzzNQo7ZZ44iHt3KbpMc 27 | q7A9LqSlof/MGGwOWdNIF4ihIn7tl00mheD5YDcWfLQicIg+VCQy672dH1Bkoj7F 28 | 0Pqob08No7LTgMCFkF9wYSQPxv5pWxst264nwc6JI8YHA0kReM0yIVr7hvLWz2AG 29 | ERdbcEhuRrJUoEL/ceyNsCm82vt9otMIK/UFLyKvMUsT7yqMnAaZ8lH9WjGSAblN 30 | xBq+RkV8Tdor2s4yhBOnXlYaQiV8DyjKjH1l3TR3of2sAvFeJA0mSbWERC50qTPW 31 | h5YCBFKL5QiZyD7ar+c7JImCJmO2M5Hb9opYYnq+wBVNOrj2/0fmq2DA9ZTs7guJ 32 | vJFTcI7HtP+K9D/FkrcWYRaISliTk+GaNCGEAUzy6uTBxTvsDgV+oXWyIX1UEhnQ 33 | nyxVMMViFvHChTgmY4EYHzF8SH4Y+PI2cTYw3B/iE9pdEyiJeqBhcpoTRYxKf3tT 34 | Yu0hBe4PauRj0qkUNxlGFU1W0E7bcG1UBiPADMQ6wLSGcuS8THC7P6vtxwqJuQNo 35 | 1LKTW4Y6htyviJ0tBgU6qnliDABsYd3qXD5t9uIha/730to= 36 | -----END CERTIFICATE----- 37 | -------------------------------------------------------------------------------- /js-backend/keys/demo/client.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/js-backend/keys/demo/client.p12 -------------------------------------------------------------------------------- /js-backend/keys/demo/server_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIGTzCCBDegAwIBAgIUJpEyBkm0fJJ+7i85bpm/UU1ZP4EwDQYJKoZIhvcNAQEL 3 | BQAwgbYxHTAbBgNVBAMMFE9XQVNQIFNpbmdsZSBTaWduLU9uMRkwFwYDVQQKDBBP 4 | V0FTUCBFdXJvcGUgVlpXMRQwEgYDVQQLDAtTU09fUHJvamVjdDEzMDEGCSqGSIb3 5 | DQEJARYkSmFtZXNDdWxsdW1AdXNlcnMubm9yZXBseS5naXRodWIuY29tMQ8wDQYD 6 | VQQHDAZCLTk2NjAxETAPBgNVBAgMCE9wYnJha2VsMQswCQYDVQQGEwJCRTAeFw0y 7 | MDAzMjExMzM2MTBaFw0yNTAzMjAxMzM2MTBaMIG2MR0wGwYDVQQDDBRPV0FTUCBT 8 | aW5nbGUgU2lnbi1PbjEZMBcGA1UECgwQT1dBU1AgRXVyb3BlIFZaVzEUMBIGA1UE 9 | CwwLU1NPX1Byb2plY3QxMzAxBgkqhkiG9w0BCQEWJEphbWVzQ3VsbHVtQHVzZXJz 10 | Lm5vcmVwbHkuZ2l0aHViLmNvbTEPMA0GA1UEBwwGQi05NjYwMREwDwYDVQQIDAhP 11 | cGJyYWtlbDELMAkGA1UEBhMCQkUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK 12 | AoICAQDhyu6Rcmd6jez5UDdn0dxpq+bsCwsmycqpjzE3qJSF+Nji8oE5TXYNNEbQ 13 | mg+cDyYUCL76VAJnxqnN2//dsqKpj1bXTtqJqe5NyLAfAz45qIqCDbd4OQCPfFPl 14 | TFI/q/zYf6ge93BqFGq4JLt43TMTeqUnvGrfckaBpSA0R3GaXpwCwvryGEx/iwLz 15 | EVSUDRFloxcNbmWTGxgqyqnEM+Ptp0FL/6hPHm6E+t7GKtuKA+z68ud16RunqCRz 16 | oVfRr4flrm0iB6gIe1NiUjsmJuFTZM7gH+o8oo59zIeLltIvlpDUmpdHkPGxEn4n 17 | oRlgiwnuRDl6qdl0SoAxHjLfnjhdmgXW6fH/c6gtciQ3s+QAKidw/dfpC1xSQIdp 18 | ByOL/OHpTzkkWD9Q8I4lxo0Rf9GdRvB9oOPA121fWKPHMzDGHQk3JTNN5Aj+u27s 19 | QW5GS8J0LnIFclpgx3yeCVAmoSViGMn15jyr/0M+wHaXGNwA5GfnHlJTI+G4CBaq 20 | y2bdoM6y/vo9PRRQpaNVvvz/Slch8zeQBVRAHnwdKXXehWC/zkP3QOctxtMkbTJ4 21 | fLPopCS+EGAa2kbForycnUDMWxwv2R/omg0ZmhaD483aGMuzmiaDsFf8mNxD1+Or 22 | eQD05ZkzH7Gvi1+z8EIVNdzCJoO7rjVY42KVWBBo2eykRoYl0QIDAQABo1MwUTAd 23 | BgNVHQ4EFgQU/Hz/m30+tORkRfoBujT+euGrdX8wHwYDVR0jBBgwFoAU/Hz/m30+ 24 | tORkRfoBujT+euGrdX8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC 25 | AgEAcMIKlI0hnnxnGSYgbZ3Xc88H3S3jxWCuCQOnnQvl9kPvYxe3CDgGM8x7e+GO 26 | odWtjmhoKtbGNidyfbCCvNJ0R6OLKYLqDQTdq/KMy2jPLzzNQo7ZZ44iHt3KbpMc 27 | q7A9LqSlof/MGGwOWdNIF4ihIn7tl00mheD5YDcWfLQicIg+VCQy672dH1Bkoj7F 28 | 0Pqob08No7LTgMCFkF9wYSQPxv5pWxst264nwc6JI8YHA0kReM0yIVr7hvLWz2AG 29 | ERdbcEhuRrJUoEL/ceyNsCm82vt9otMIK/UFLyKvMUsT7yqMnAaZ8lH9WjGSAblN 30 | xBq+RkV8Tdor2s4yhBOnXlYaQiV8DyjKjH1l3TR3of2sAvFeJA0mSbWERC50qTPW 31 | h5YCBFKL5QiZyD7ar+c7JImCJmO2M5Hb9opYYnq+wBVNOrj2/0fmq2DA9ZTs7guJ 32 | vJFTcI7HtP+K9D/FkrcWYRaISliTk+GaNCGEAUzy6uTBxTvsDgV+oXWyIX1UEhnQ 33 | nyxVMMViFvHChTgmY4EYHzF8SH4Y+PI2cTYw3B/iE9pdEyiJeqBhcpoTRYxKf3tT 34 | Yu0hBe4PauRj0qkUNxlGFU1W0E7bcG1UBiPADMQ6wLSGcuS8THC7P6vtxwqJuQNo 35 | 1LKTW4Y6htyviJ0tBgU6qnliDABsYd3qXD5t9uIha/730to= 36 | -----END CERTIFICATE----- 37 | -------------------------------------------------------------------------------- /js-backend/keys/demo/server_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDhyu6Rcmd6jez5 3 | UDdn0dxpq+bsCwsmycqpjzE3qJSF+Nji8oE5TXYNNEbQmg+cDyYUCL76VAJnxqnN 4 | 2//dsqKpj1bXTtqJqe5NyLAfAz45qIqCDbd4OQCPfFPlTFI/q/zYf6ge93BqFGq4 5 | JLt43TMTeqUnvGrfckaBpSA0R3GaXpwCwvryGEx/iwLzEVSUDRFloxcNbmWTGxgq 6 | yqnEM+Ptp0FL/6hPHm6E+t7GKtuKA+z68ud16RunqCRzoVfRr4flrm0iB6gIe1Ni 7 | UjsmJuFTZM7gH+o8oo59zIeLltIvlpDUmpdHkPGxEn4noRlgiwnuRDl6qdl0SoAx 8 | HjLfnjhdmgXW6fH/c6gtciQ3s+QAKidw/dfpC1xSQIdpByOL/OHpTzkkWD9Q8I4l 9 | xo0Rf9GdRvB9oOPA121fWKPHMzDGHQk3JTNN5Aj+u27sQW5GS8J0LnIFclpgx3ye 10 | CVAmoSViGMn15jyr/0M+wHaXGNwA5GfnHlJTI+G4CBaqy2bdoM6y/vo9PRRQpaNV 11 | vvz/Slch8zeQBVRAHnwdKXXehWC/zkP3QOctxtMkbTJ4fLPopCS+EGAa2kbForyc 12 | nUDMWxwv2R/omg0ZmhaD483aGMuzmiaDsFf8mNxD1+OreQD05ZkzH7Gvi1+z8EIV 13 | NdzCJoO7rjVY42KVWBBo2eykRoYl0QIDAQABAoICAQCFOoaWT9j7GW9wlIJ8wfm6 14 | aHIzaTjFRGBOuG0kGFa6zmC7WLgjJa+4jtYSFDWNseqX+6kkcBmTPkfHbj6Fg/gy 15 | 8J8fICoW/KvePcKeKf23a9l+b7WIPKo1//hI3kXRyBvDa1+6FerAzRdDXHk6Edsn 16 | bUCyN50gB3/O064y6sz/dz+66W3FF81bnJy668jqKuPPAvKBPFi2+k/CQB735F5j 17 | RkNjIpEfTieMU7LLX1J3F2XnlRg6HLfyr2YCzHrTAS3AurEuWTIu/wXuh/Layio+ 18 | WMcwu+QsrwZA+EQqe22+IquGp0kIM2BJSU4i+A3PR3sumhcIZ9wloCgP02/VPrim 19 | dpFupY0kdHZzWvb6CZlq8NWCpLqQSGJ6JBZLFfcuAkIQJnylWwuavi2pV/CWNJ0/ 20 | 2j+b46zQy9lVDQ/RmBi3gHMfgfVCmC556gmyp8oTmwzPbIXxcl1qaFxnV0YI2jg4 21 | sJiqDk9dJpp4jeBqaso2mqEVMs39SHUjB8hAZ1dMFvRcUHtStnaVYXGLm+An22wb 22 | Yicjq8oJuf11wBuSXmYRqr2T7ZsQfW/DyxMsFXzLQd+9it+enmMFSYxi/j7A9mgK 23 | fIb5DOyXYGvauBMAZUMulldn+QYQkHP7imf7bR1BRbHZGhZMPgLSkMvF4ftZioVm 24 | Tdz5QLRmuXg51TkplV/c2QKCAQEA84+rw2pXCe7A5QewztlzW4QJvU4CEPHv/BxY 25 | /NcUMdnqfTfHS9dmgreuJWtBIrns1tQ1t9pjxO+BDGprbT2JU5tBki7mQMjQXYP0 26 | f6lizd19TJrsnSqDflaUTRNlZoBuZ80uSjsn/vaPDsFEvtSDn+GNCi9lu7mcBao+ 27 | BGN+xibdLIB5k6RVdUindCAiviwoxFRj2yEnzI4HVF3GVsU6z7F+hJjraItgjGzs 28 | +0+3EMdhy8c7c7gwEIpHYcxLVXAdLhcMhOonCVn26HcgzZlpcLCQW1FHfk2I1uK9 29 | NZGxhFNdgXawSSgLByxtUR+DOP7MzMRIJLw7QVteduEgP5CLAwKCAQEA7VL0aKqS 30 | hCi5ARSaRktbf7XS7vHAgk7u8Oi+laaD1QpWEeJbGqwrqkbahf10jEID4ZibFxg+ 31 | Eos1Y7F4Nr3eY7NdaQpiwXiY3jEcKGBHsx4Nm1p3gvxhmM9KOwsQN/nXoT/B/TTT 32 | OyDhQ2kiqmSw/eCBe3tbvBw5Gv0qfHj8wWqu9NzpnBvySpBSxDltAFaD4tvFu3Jp 33 | u+xrMfBsbg1EFd3iMxBmdTXy6j7Klz7FlXaZpUhtp1gF/XwC4xQDrrC9phYaTJNl 34 | FjQF58WD11V7tcfka7g/nqN2+RbRZHbp2z9oXC3LXj1twXDqkq5rfsxVD3gxy2Oj 35 | XGyKqXaVjb+pmwKCAQBnDUXeg9LZ6AQDo7Jigsz1TrOUPjpPkx44LIJWUGZCBXLa 36 | kkwnwbak3jS3rl6747Da4KTt6mBGRhPy/eAAM4Y0Mr+Wq3NOu+i1eIxtq2ybr/hA 37 | /lfY09EZFmbfCbLgoLyV/NF2JPtyPD4hPxLoLyCV+CflxFImEI18NCDTWImeK3lv 38 | R9io6GIkIp5/Ws14a1TAZPhvEeM2AG4HezndIswUboQadF0+OaKbJ8dJOdw6JDIz 39 | t1NnTepARVGYhojlWG9wPd5VKoFjyoXWq/jcJOng/UPG72fxf7rDOOJXlRJn66Hn 40 | kiFagq/P1DNYnJ238s8SYIYpzN7Bc3hPkkdDvdOjAoIBADrexhQko922wFW9R5vC 41 | W7stXlq5i3iHngwSoBw9RxyUyM786H6QUPCXTOPKAJw+T/opQeeTsuoksCS/xENI 42 | DB+NQdGpqt+1S65qXkLBx2duQA/WfCj49DjUyuQklWxdRJkSWMu3y+IyAM8ZCGle 43 | Ou1vQI9iBVp/YccJH/3qgUB1d1r0Zdq8r/jBHlbgq+JWk7a1r7IU6pecSOcFbdBK 44 | 6CXIebETE2dl75Ed7GFiR98QoYJ3mgPV+P5tQcYUk+lEnHbTqshuE7hTRVe+Djtf 45 | UCXCpS33cYinof6eI2kZel9RtlCPZ5BECrLekMG8FJxKkZsZxLhgSgjGQAtTiDu2 46 | XJsCggEADPq8AYxeUjs4XgKnLRxxME6aJCpb5u/YxIuQgIwnMaidnMBvbYKb0BMa 47 | G6yDQ7qOyUwgYVcyx3LlkCk/W5Oua4VdxhUlt3AfFZRD+apb/foofdWiCqB14ukS 48 | uPK9Dynq2zQrr88BhBM5HPmyAOJUDQRWf86TiyJQb5aVvlFcDXa6n60faELu3FuD 49 | XT+9rCWfIdxBDXh2lrk7acvgB+HUVL0wYrXsOSotFKkLbuuuJC47lPQIxQ8kpMjV 50 | FzxWhNyf4SiQAD5lwQbd6/Rtvmzid6zZyGaXJMTw5Uzxp0ZoMSqkmFFZGpqyxjcI 51 | BElXaCegdfotUeebgLo0MDqefXepMg== 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /js-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OWASP-SSO-Backend", 3 | "version": "1.0.0", 4 | "author": "JamesCullum (https://github.com/JamesCullum)", 5 | "license": "GPL-3.0-or-later", 6 | "description": "NodeJS backend for the OWASP Single Sign-On project", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/OWASP/SSO_Project.git", 10 | "directory": "js-backend" 11 | }, 12 | "scripts": { 13 | "serve": "node index.js", 14 | "lint": "eslint .", 15 | "lint:fix": "eslint --fix .", 16 | "test:unit": "mocha \"tests/unit/**/*.spec.js\" --timeout 2000 --exit" 17 | }, 18 | "dependencies": { 19 | "argon2": "^0.26.2", 20 | "base64-arraybuffer": "^0.2.0", 21 | "body-parser": "^1.19.0", 22 | "dotenv": "^8.2.0", 23 | "express": "^4.17.1", 24 | "express-rate-limit": "^5.1.3", 25 | "fido2-library": "^2.5.1", 26 | "ip-country": "^1.0.2", 27 | "jsonwebtoken": "^8.5.1", 28 | "mysql2": "^1.7.0", 29 | "node-forge": "^1.0.0", 30 | "nodemailer": "^6.6.1", 31 | "samlp": "^6.0.1", 32 | "syslog-pro": "^1.0.0", 33 | "validator": "^13.7.0" 34 | }, 35 | "devDependencies": { 36 | "babel-eslint": "^10.1.0", 37 | "chai": "^4.2.0", 38 | "eslint": "^6.8.0", 39 | "eslint-plugin-vue": "^6.2.2", 40 | "mocha": "^7.2.0", 41 | "sinon": "^9.0.2" 42 | }, 43 | "eslintConfig": { 44 | "root": true, 45 | "env": { 46 | "node": true 47 | }, 48 | "extends": [ 49 | "plugin:vue/recommended" 50 | ], 51 | "rules": { 52 | "no-mixed-spaces-and-tabs": "error", 53 | "quotes": [ 54 | "error", 55 | "double", 56 | { 57 | "avoidEscape": true 58 | } 59 | ], 60 | "comma-dangle": [ 61 | "error", 62 | { 63 | "arrays": "always-multiline", 64 | "objects": "always-multiline", 65 | "imports": "never", 66 | "exports": "never", 67 | "functions": "ignore" 68 | } 69 | ], 70 | "indent": [ 71 | "error", 72 | "tab", 73 | { 74 | "SwitchCase": 1 75 | } 76 | ], 77 | "semi": [ 78 | "error", 79 | "always" 80 | ], 81 | "no-multiple-empty-lines": [ 82 | "error", 83 | { 84 | "max": 2, 85 | "maxEOF": 1 86 | } 87 | ] 88 | }, 89 | "parserOptions": { 90 | "parser": "babel-eslint" 91 | }, 92 | "overrides": [ 93 | { 94 | "files": [ 95 | "**/__tests__/*.{j,t}s?(x)" 96 | ], 97 | "env": { 98 | "mocha": true 99 | } 100 | } 101 | ] 102 | }, 103 | "browserslist": [ 104 | "> 1%", 105 | "last 2 versions" 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /js-backend/scripts/create-client.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export MSYS_NO_PATHCONV=1 # win git bash workaround 3 | 4 | TMPID=tmp/$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1) 5 | TIMESTAMP=$(date +%s%N) 6 | CACRT=keys/server_cert.pem 7 | CAKEY=keys/server_key.pem 8 | 9 | openssl req -newkey rsa:4096 -keyout $TMPID.key -out $TMPID.csr -nodes -subj "/CN=$1/emailAddress=$2" >/dev/null 2>&1 10 | openssl x509 -req -in $TMPID.csr -CA $CACRT -CAkey $CAKEY -out $TMPID.crt -set_serial $TIMESTAMP -days 1095 -extfile scripts/v3.ext >/dev/null 2>&1 11 | FINGERPRINT=$(openssl x509 -in $TMPID.crt -noout -sha256 -fingerprint | cut -c 20-) 12 | cat $CACRT >> $TMPID.crt 13 | openssl pkcs12 -export -in $TMPID.crt -inkey $TMPID.key -out $TMPID.p12 -passout pass: >/dev/null 2>&1 14 | rm $TMPID.key $TMPID.csr $TMPID.crt >/dev/null 15 | echo -e "{\"file\":\"$TMPID.p12\", \"fingerprint256\":\"$FINGERPRINT\"}" -------------------------------------------------------------------------------- /js-backend/scripts/setup.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export MSYS_NO_PATHCONV=1 # win git bash workaround 3 | 4 | if [ ! -f ../keys/server_key.pem ]; then 5 | echo "Server key not found, generating for $1..." 6 | 7 | openssl req -x509 -newkey rsa:4096 -keyout keys/server_key.pem -out keys/server_cert.pem -nodes -days 1825 -subj "/CN=$1/O=OWASP Europe VZW/OU=SSO_Project/emailAddress=JamesCullum@users.noreply.github.com/L=B-9660/ST=Opbrakel/C=BE" 8 | fi -------------------------------------------------------------------------------- /js-backend/scripts/v3.ext: -------------------------------------------------------------------------------- 1 | authorityKeyIdentifier=keyid,issuer 2 | basicConstraints=CA:FALSE 3 | extendedKeyUsage = serverAuth, clientAuth, emailProtection 4 | keyUsage=nonRepudiation, digitalSignature, keyEncipherment -------------------------------------------------------------------------------- /js-backend/tests/unit/_shared.js: -------------------------------------------------------------------------------- 1 | const sinon = require("sinon"); 2 | 3 | const db = { 4 | execute: sinon.stub().callsArgWith(2, null, { 5 | insertId: "insertId", 6 | }), 7 | }; 8 | process.env.MOCKDB = db; // Allows to include utils/index.js 9 | process.env.UNIQUEJWTTOKEN = "key"; 10 | 11 | const { User, Audit, PwUtil, Mailer, JWT } = require("../../utils"); 12 | 13 | const pages = require("../../websites.json"); 14 | pages["default"].syslog = "default-syslog"; 15 | pages["1"].syslog = "1-syslog"; 16 | pages["2"] = { 17 | syslog: "2-syslog", 18 | }; 19 | 20 | module.exports = { 21 | pages, 22 | db, 23 | res: { 24 | status: sinon.stub().returnsThis(), 25 | send: sinon.stub().returnsThis(), 26 | json: sinon.stub().returnsThis(), 27 | }, 28 | fido2Options: { 29 | updateCounter: sinon.stub(), 30 | readUser: sinon.stub(), 31 | addAuthenticator: sinon.stub(), 32 | verifyUser: sinon.stub(), 33 | rpId: "example", 34 | protocol: "https", 35 | origin: "https://example.com", 36 | }, 37 | stubs: { 38 | // Environment variables are used to prevent classes being mocked 39 | auditAddStub: process.env.DONTMOCKAUDIT ? null : sinon.stub(Audit, "add").resolves(1), 40 | getIPStub: process.env.DONTMOCKAUDIT ? null : sinon.stub(Audit, "getIP").returns("127.0.0.1"), 41 | findUserStub: process.env.DONTMOCKUSER ? null : sinon.stub(User, "findUserByName"), // by name 42 | userFindStub: process.env.DONTMOCKUSER ? null : sinon.stub(User, "findUserById"), // by id 43 | activationStub: process.env.DONTMOCKUSER ? null : sinon.stub(User, "requestEmailActivation").resolves("token"), 44 | resolveStub: process.env.DONTMOCKUSER ? null : sinon.stub(User, "resolveEmailActivation").resolves({ 45 | id: 1, 46 | username: "username", 47 | }), 48 | addAuthStub: process.env.DONTMOCKUSER ? null : sinon.stub(User, "addAuthenticator").resolves(), 49 | validateSessionStub: process.env.DONTMOCKUSER ? null : sinon.stub(User, "validateSession"), 50 | mailStub: process.env.DONTMOCKMAILER ? null : sinon.stub(Mailer, "sendMail").callsArgWith(1, null), 51 | checkPassStub: process.env.DONTMOCKPWUTIL ? null : sinon.stub(PwUtil, "checkPassword").resolves(), 52 | jwtSignStub: process.env.DONTMOCKJWT ? null : sinon.stub(JWT, "sign").resolves("jwt"), 53 | jwtVerifyStub: process.env.DONTMOCKJWT ? null : sinon.stub(JWT, "verify"), 54 | }, 55 | }; 56 | 57 | -------------------------------------------------------------------------------- /js-backend/tests/unit/flows/audit.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const sinon = require("sinon"); 3 | 4 | const { res, stubs } = require("../_shared.js"); 5 | const { Audit } = require("../../../utils"); 6 | 7 | const AuditFlowClass = require("../../../flows/audit.js").auditFlow; 8 | const AuditFlow = new AuditFlowClass(); 9 | 10 | const userData = { 11 | id: 1, 12 | username: "username", 13 | password: "password", 14 | authenticators: [{ 15 | userCounter: "userCounter", 16 | userKey: "userKey", 17 | userHandle: "userHandle", 18 | }], 19 | }; 20 | 21 | describe("Audit (Flow)", () => { 22 | it("gets information about logged in user", done => { 23 | stubs.userFindStub.resetHistory().resolves(userData); 24 | stubs.validateSessionStub.resetHistory(); 25 | 26 | res.json = data => { 27 | expect(stubs.userFindStub.calledWith(1)).to.equal(true); 28 | expect(stubs.validateSessionStub.called).to.equal(false); 29 | 30 | expect(data.isAuthenticated).to.equal(false); 31 | expect(data.hasOwnProperty("password")).to.equal(false); 32 | const authenticator = data.authenticators[0]; 33 | expect(authenticator.userHandle).to.equal("userHandle"); 34 | expect(authenticator.hasOwnProperty("userKey")).to.equal(false); 35 | 36 | done(); 37 | }; 38 | 39 | AuditFlow.getMe({ 40 | user: { 41 | id: 1, 42 | }, 43 | }, res); 44 | }); 45 | 46 | it("gets information about authenticated user", done => { 47 | stubs.userFindStub.resetHistory().resolves(userData); 48 | stubs.validateSessionStub.resetHistory().resolves({ userId: 1 }); 49 | 50 | res.json = data => { 51 | expect(stubs.userFindStub.calledWith(1)).to.equal(true); 52 | expect(stubs.validateSessionStub.calledWith("token")).to.equal(true); 53 | 54 | expect(data.isAuthenticated).to.equal(true); 55 | expect(data.hasOwnProperty("password")).to.equal(false); 56 | const authenticator = data.authenticators[0]; 57 | expect(authenticator.userHandle).to.equal("userHandle"); 58 | expect(authenticator.hasOwnProperty("userKey")).to.equal(false); 59 | 60 | done(); 61 | }; 62 | 63 | AuditFlow.getMe({ 64 | user: { 65 | id: 1, 66 | token: "token", 67 | }, 68 | }, res); 69 | }); 70 | 71 | it("gets audit log list", done => { 72 | const auditGetListStub = sinon.stub(Audit, "getList").resolves([]); 73 | process.env.AUDITPAGELENGTH = 7; 74 | 75 | res.json = data => { 76 | expect(auditGetListStub.calledWith(1, 14, 7)).to.equal(true); 77 | 78 | done(); 79 | }; 80 | 81 | AuditFlow.getAuditLogs({ 82 | query: { 83 | page: 2, 84 | }, 85 | user: { 86 | id: 1, 87 | }, 88 | }, res); 89 | }); 90 | 91 | it("can report an audit event", done => { 92 | stubs.auditAddStub.resetHistory(); 93 | res.status.resetHistory(); 94 | 95 | const auditGetStub = sinon.stub(Audit, "get").resolves([{ 96 | user: 1, 97 | }]); 98 | 99 | AuditFlow.reportAuditLog({ 100 | user: { 101 | id: 2, 102 | }, 103 | body: { 104 | id: 1, 105 | }, 106 | }, res, () => { 107 | expect.fail("Wrong user was able to report event"); 108 | }); 109 | 110 | setTimeout(() => { 111 | expect(auditGetStub.calledWith(1)).to.equal(true); 112 | expect(stubs.auditAddStub.called).to.equal(false); 113 | expect(res.status.calledWith(404)).to.equal(true); 114 | res.status.resetHistory(); 115 | 116 | // Now correct one 117 | AuditFlow.reportAuditLog({ 118 | user: { 119 | id: 1, 120 | }, 121 | body: { 122 | id: 1, 123 | }, 124 | }, res, () => { 125 | expect(stubs.auditAddStub.called).to.equal(true); 126 | expect(res.status.calledWith(404)).to.equal(false); 127 | 128 | done(); 129 | }); 130 | }, 0); 131 | }); 132 | }); -------------------------------------------------------------------------------- /js-backend/tests/unit/flows/local-auth.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const sinon = require("sinon"); 3 | 4 | const { res, stubs } = require("../_shared.js"); 5 | const { User, PwUtil } = require("../../../utils"); 6 | 7 | const LocalAuthClass = require("../../../flows/local-auth.js").localAuthFlow; 8 | const LocalAuth = new LocalAuthClass(); 9 | 10 | describe("Local-Auth (Flow)", () => { 11 | it("initializes correctly", () => { 12 | expect(LocalAuth.hostname).to.equal("localhost"); 13 | }); 14 | 15 | it("allows login with username and password", done => { 16 | stubs.auditAddStub.resetHistory(); 17 | stubs.findUserStub.resetHistory().resolves({ 18 | id: 1, 19 | password: "password", 20 | }); 21 | const verifyStub = sinon.stub(PwUtil, "verifyPassword").resolves(true); 22 | const updateStub = sinon.stub(User, "updateLoginTime").resolves(true); 23 | 24 | const req = { 25 | body: { 26 | username: "username", 27 | password: "password", 28 | }, 29 | }; 30 | 31 | LocalAuth.onLocalLogin(req, res, () => { 32 | expect(stubs.findUserStub.calledWith("username")).to.equal(true); 33 | expect(verifyStub.calledWith("password", "password")).to.equal(true); 34 | expect(stubs.auditAddStub.called).to.equal(true); 35 | expect(req.loginEmail).to.equal("username"); 36 | 37 | done(); 38 | }); 39 | }); 40 | 41 | it("allows email confirmation request", done => { 42 | stubs.activationStub.resetHistory(); 43 | stubs.mailStub.resetHistory(); 44 | 45 | LocalAuth.onLocalEmailAuth({ 46 | user: { 47 | username: "username", 48 | }, 49 | }, res, () => { 50 | expect(stubs.activationStub.calledWith("username", sinon.match.any, "login")).to.equal(true); 51 | expect(stubs.mailStub.called).to.equal(true); 52 | 53 | done(); 54 | }); 55 | }); 56 | 57 | it("confirms email confirmation request", done => { 58 | stubs.resolveStub.resetHistory(); 59 | 60 | const req = { 61 | query: { 62 | token: "token", 63 | action: "login", 64 | }, 65 | }; 66 | LocalAuth.onEmailConfirm(req, res, () => { 67 | expect(stubs.resolveStub.calledWith("token", sinon.match.any, "login")).to.equal(true); 68 | 69 | done(); 70 | }); 71 | }); 72 | 73 | it("allows registration request", done => { 74 | stubs.activationStub.resetHistory(); 75 | stubs.mailStub.resetHistory(); 76 | 77 | LocalAuth.onRegister({ 78 | body: { 79 | email: "email", 80 | }, 81 | }, res, () => { 82 | expect(stubs.activationStub.calledWith("email", sinon.match.any, "registration")).to.equal(true); 83 | expect(stubs.mailStub.called).to.equal(true); 84 | 85 | done(); 86 | }); 87 | }); 88 | 89 | it("allows password change request", done => { 90 | stubs.activationStub.resetHistory(); 91 | stubs.mailStub.resetHistory(); 92 | 93 | LocalAuth.onChangeRequest({ 94 | body: { 95 | email: "email", 96 | }, 97 | }, res, () => { 98 | expect(stubs.activationStub.calledWith("email", sinon.match.any, "change")).to.equal(true); 99 | expect(stubs.mailStub.called).to.equal(true); 100 | 101 | done(); 102 | }); 103 | }); 104 | 105 | it("confirms registration", done => { 106 | stubs.checkPassStub.resetHistory(); 107 | stubs.resolveStub.resetHistory(); 108 | 109 | const req = { 110 | body: { 111 | token: "token", 112 | password: "password", 113 | }, 114 | }; 115 | LocalAuth.onActivate(req, res, () => { 116 | expect(stubs.checkPassStub.calledWith(null, "password")).to.equal(true); 117 | expect(stubs.resolveStub.calledWith("token", sinon.match.any, "registration")).to.equal(true); 118 | expect(req.body.username).to.equal("username"); 119 | 120 | done(); 121 | }); 122 | }); 123 | 124 | it("confirms password change", done => { 125 | stubs.checkPassStub.resetHistory(); 126 | stubs.resolveStub.resetHistory(); 127 | stubs.findUserStub.resetHistory(); 128 | 129 | const invalidateStub = sinon.stub(User, "manualInvalidateToken").resolves(); 130 | const changeStub = sinon.stub(User, "changePassword").resolves(); 131 | 132 | const req = { 133 | body: { 134 | token: "token", 135 | password: "password", 136 | }, 137 | }; 138 | LocalAuth.onChange(req, res, () => { 139 | expect(stubs.resolveStub.calledWith("token", sinon.match.any, "change")).to.equal(true); 140 | expect(stubs.checkPassStub.calledWith(1, "password")).to.equal(true); 141 | expect(stubs.findUserStub.calledWith("username")).to.equal(true); 142 | expect(invalidateStub.calledWith(1)).to.equal(true); 143 | expect(changeStub.calledWith(1, "password")).to.equal(true); 144 | expect(req.user.id).to.equal(1); 145 | 146 | done(); 147 | }); 148 | }); 149 | }); -------------------------------------------------------------------------------- /js-backend/tests/unit/flows/sso-flow.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const sinon = require("sinon"); 3 | 4 | const { res, pages, fido2Options, stubs } = require("../_shared.js"); 5 | const { Audit, User, JWT } = require("../../../utils"); 6 | const samlp = require("samlp"); 7 | // We can't mock MiddlewareHelper, but can catch the result using "res" 8 | 9 | const samlParseStub = sinon.stub(samlp, "parseRequest").callsArgWith(1, null, { 10 | assertionConsumerServiceURL: "https://postman-echo.com/post?saml", 11 | }); 12 | 13 | const SSOFlowClass = require("../../../flows/sso-flow.js").ssoFlow; 14 | const SSOFlow = new SSOFlowClass(pages, fido2Options, "serverCrt", "serverKey"); 15 | 16 | describe("SSO-Flow (Flow)", () => { 17 | it("initializes correctly", () => { 18 | expect(SSOFlow.ownJwtToken).to.equal("key"); 19 | expect(SSOFlow.serverCrt).to.equal("serverCrt"); 20 | }); 21 | 22 | it("allows an unauthenticated incoming JWT flow", done => { 23 | stubs.jwtSignStub.resetHistory(); 24 | 25 | res.json = data => { 26 | expect(stubs.jwtSignStub.called).to.equal(true); 27 | expect(data.page.token).to.equal("jwt"); 28 | expect(data.page.flowType).to.equal("jwt"); 29 | 30 | done(); 31 | }; 32 | 33 | SSOFlow.onFlowIn({ 34 | query: { 35 | id: 1, 36 | }, 37 | body: {}, 38 | }, res, null); 39 | }); 40 | 41 | it("allow an authenticated incoming JWT flow", done => { 42 | stubs.auditAddStub.resetHistory(); 43 | stubs.findUserStub.resetHistory().resolves({ 44 | id: 1, 45 | }); 46 | 47 | const sampleMail = "username@example.com"; 48 | stubs.jwtVerifyStub.resetHistory().resolves({ 49 | sub: sampleMail, 50 | }); 51 | 52 | const req = { 53 | query: { 54 | id: 1, 55 | d: "jwt", 56 | }, 57 | body: {}, 58 | }; 59 | 60 | res.json = data => { 61 | expect(req.user.id).to.equal(1); 62 | expect(req.loginEmail).to.equal(sampleMail); 63 | 64 | expect(stubs.auditAddStub.called).to.equal(true); 65 | expect(stubs.jwtSignStub.called).to.equal(true); 66 | expect(data.token).to.be.a("string"); 67 | expect(data.username).to.equal(sampleMail); 68 | expect(data.factor).to.equal(1); 69 | expect(data.page.token).to.equal("jwt"); 70 | expect(data.page.flowType).to.equal("jwt"); 71 | 72 | done(); 73 | }; 74 | 75 | SSOFlow.onFlowIn(req, res, null); 76 | }); 77 | 78 | it("allows authenticated outgoing JWT flow", done => { 79 | stubs.auditAddStub.resetHistory(); 80 | stubs.jwtSignStub.resetHistory(); 81 | 82 | const req = { 83 | ssoRequest: { 84 | pageId: 1, 85 | sub: "other-username", 86 | jwt: true, 87 | }, 88 | user: { 89 | id: 1, 90 | username: "username", 91 | }, 92 | }; 93 | 94 | // Check rejection 95 | res.status.resetHistory(); 96 | res.json = data => { 97 | expect.fail("Accepted wrong user"); 98 | }; 99 | SSOFlow.onFlowOut(req, res, null); 100 | expect(res.status.calledWith(403)).to.equal(true); 101 | 102 | // Check correct one 103 | req.ssoRequest.sub = "username"; 104 | res.json = data => { 105 | expect(data.token).to.equal("jwt"); 106 | expect(data.redirect).to.be.a("string"); 107 | expect(stubs.auditAddStub.called).to.equal(true); 108 | expect(stubs.jwtSignStub.called).to.equal(true); 109 | 110 | done(); 111 | }; 112 | 113 | SSOFlow.onFlowOut(req, res, null); 114 | }); 115 | 116 | it("allows outgoing SAML flow", done => { 117 | samlParseStub.resetHistory(); 118 | stubs.jwtSignStub.resetHistory(); 119 | 120 | res.json = data => { 121 | expect(data.page.token).to.equal("jwt"); 122 | expect(data.page.flowType).to.equal("saml"); 123 | expect(samlParseStub.called).to.equal(true); 124 | expect(stubs.jwtSignStub.called).to.equal(true); 125 | 126 | done(); 127 | }; 128 | 129 | SSOFlow.onSamlIn({ 130 | query: { 131 | SAMLRequest: "fZJZc6owGIb%2FCpN7EMEFMqKDuC8VhVr1xokhsggJJcGlv75WjzM9N71M8n55knmfVueapdKZFDxm1AJVRQUSoZgFMQ0t8O4PZAN02i2OsjSHdikiuiKfJeFCus9RDh8HFigLChniMYcUZYRDgaFnz2dQU1SYF0wwzFIg2ZyTQtxBDqO8zEjhkeIcY%2FK%2BmlkgEiLnsFLJGRcZojLBEVMwyx4bnR8OkHp3cEyReLz1NRDGIioPj%2Bjiw%2Fbciuct9m7BEoIFkMY9C%2By1t%2FpwmGs0G%2FvO4IBvQ2%2BaoKWO4sb0amL70ou6w001tCe3s1OkkzPrL4OkH%2Fr8iJJwbmbNRLkoY3PwVQ2CdbIz0%2BnOiTDPRW6erien%2FMSXA3kjSjq5rZd514g9VEOXwZYnpl0vmqv%2BoT7JR6Om1m0cSViOBgO3b8zRSS9rkdefpubeyFi8mQV2vfmxjr4uyNhsr7SW9Xjk5frmoLr93XTRG83FNV1v0A0Pt%2BU8dDR1mB8d%2B%2F5LzksyplwgKiygqVVdVmuyZviaBms6rGmKbjR2QHL%2FddGN6bPhv4o7PEMcjnzfld2F5wNp%2FTLlHgBPL%2BADXvwS4u9r0csC0H5ViO5iqXJAzjIJlOymcJQSfmQFJj%2B1tiq%2FMO3n6n8Z298%3D", 132 | RelayState: "RelayState", 133 | }, 134 | }, res, null); 135 | }); 136 | 137 | it("shows saml meta", done => { 138 | samlp.metadata = data => { 139 | expect(data.issuer).to.equal(fido2Options.rpName); 140 | expect(data.cert).to.equal("serverCrt"); 141 | 142 | done(); 143 | }; 144 | 145 | SSOFlow.onSamlMeta(null, null, null); 146 | }); 147 | }); -------------------------------------------------------------------------------- /js-backend/tests/unit/utils/audit.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const sinon = require("sinon"); 3 | 4 | process.env.DONTMOCKAUDIT = true; 5 | const { db, pages, stubs } = require("../_shared.js"); 6 | db.execute.callsArgWith(2, null, { 7 | insertId: "insertId", 8 | }); 9 | 10 | const AuditClass = require("../../../utils/audit.js").Audit; 11 | const Audit = new AuditClass(db, {}); 12 | 13 | const cefStub = sinon.stub(Audit, "cefSend"); 14 | cefStub.resolves({}); 15 | 16 | describe("Audit (Util)", () => { 17 | it("prepares loggers", () => { 18 | expect(Audit.customPages).to.equal(undefined); 19 | expect(Audit.version).to.equal(undefined); 20 | expect(Audit.heartbeatTimer).to.equal(undefined); 21 | expect(cefStub.called).to.equal(false); 22 | 23 | Audit.prepareLoggers(pages, "version"); 24 | 25 | expect(cefStub.calledWith("default-syslog")).to.equal(true); 26 | expect(cefStub.calledWith("2-syslog")).to.equal(true); 27 | expect(Audit.heartbeatTimer).to.not.equal(undefined); 28 | }); 29 | 30 | it("has a heartbeat", () => { 31 | cefStub.resetHistory(); 32 | Audit.heartbeat(); 33 | 34 | expect(cefStub.calledWith("default-syslog")).to.equal(true); 35 | expect(cefStub.calledWith("2-syslog")).to.equal(true); 36 | }); 37 | 38 | it("adds an entry", async () => { 39 | cefStub.resetHistory(); 40 | expect(db.execute.called).to.equal(false); 41 | 42 | const insertId = await Audit.add({ 43 | user: { 44 | id: 1, 45 | }, 46 | connection: { 47 | remoteAddress: "127.0.0.1", 48 | }, 49 | ssoRequest: { 50 | pageId: 1, 51 | }, 52 | headers: {}, 53 | }, "object", "action", "attribute"); 54 | 55 | expect(db.execute.called).to.equal(true); 56 | expect(cefStub.calledWith("default-syslog")).to.equal(true); 57 | expect(cefStub.calledWith("1-syslog")).to.equal(true); 58 | expect(cefStub.calledWith("2-syslog")).to.equal(false); 59 | expect(insertId).to.equal("insertId"); 60 | }); 61 | 62 | it("gets a list of entries", async () => { 63 | const results = await Audit.getList("userId", "offset", "length"); 64 | expect(results.insertId).to.equal("insertId"); 65 | }); 66 | 67 | it("gets one entry", async () => { 68 | const results = await Audit.get(1); 69 | expect(results.insertId).to.equal("insertId"); 70 | }); 71 | 72 | it("gets the correct ip", () => { 73 | const baseItem = { 74 | connection: { 75 | remoteAddress: "127.0.0.1", 76 | }, 77 | headers: {}, 78 | }; 79 | expect(Audit.getIP(baseItem)).to.equal("127.0.0.1"); 80 | 81 | baseItem.headers["x-forwarded-for"] = "::1"; 82 | expect(Audit.getIP(baseItem)).to.equal("::1"); 83 | 84 | baseItem.headers["x-forwarded-for"] = "::1,::2"; 85 | expect(Audit.getIP(baseItem)).to.equal("::1"); 86 | }); 87 | }); -------------------------------------------------------------------------------- /js-backend/tests/unit/utils/jwt.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const sinon = require("sinon"); 3 | 4 | const JwtClass = require("../../../utils/jwt.js").JWTHandler; 5 | const JWT = new JwtClass(); 6 | const jwtRaw = require("jsonwebtoken"); 7 | 8 | const testData = { 9 | test: "test", 10 | }; 11 | const testEncoded = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlzcyI6ImxvY2FsaG9zdCIsImF1ZCI6ImxvY2FsaG9zdCIsImlhdCI6MTU4NjI2NTk3MiwiZXhwIjo0NzQyMDI1OTcyfQ.r0fUhecvGDyWgK2k04E97KZGuaXDErY8oN3arFJh8ug"; 12 | 13 | describe("JWT (Util)", () => { 14 | it("loads hostname", () => { 15 | expect(JWT.hostname).to.equal("localhost"); 16 | }); 17 | 18 | it("has three age defaults", () => { 19 | const ages = JWT.age(); 20 | 21 | expect(ages.SHORT).to.be.a("string"); 22 | expect(ages.MEDIUM).to.be.a("string"); 23 | expect(ages.LONG).to.be.a("string"); 24 | }); 25 | 26 | it("signs data", async () => { 27 | const signedShort = await JWT.sign(testData, "key1", "5m"); 28 | const decoded = jwtRaw.decode(signedShort); 29 | 30 | expect(decoded.test).to.equal("test"); 31 | expect(decoded.iss).to.equal("localhost"); 32 | expect(decoded.aud).to.equal("localhost"); 33 | expect(decoded.exp - decoded.iat).to.equal(5 * 60); 34 | }); 35 | 36 | it("verifies data", async () => { 37 | const verified = await JWT.verify(testEncoded, "key1", { 38 | maxAge: "100y", 39 | }); 40 | 41 | expect(verified.test).to.equal("test"); 42 | expect(verified.iss).to.equal("localhost"); 43 | expect(verified.aud).to.equal("localhost"); 44 | }); 45 | 46 | it("signs and verifies its own data", async () => { 47 | const shortTime = 5*60; 48 | const mediumTime = 60*60; 49 | const longTime = 365*24*60*60; 50 | const now = Math.floor(Date.now() / 1000) - 5; 51 | 52 | const signedShort = await JWT.sign(testData, "key1", shortTime); 53 | const signedMedium = await JWT.sign(testData, "key2", mediumTime); 54 | const signedLong = await JWT.sign(testData, "key3", longTime); 55 | 56 | const verifiedShort = await JWT.verify(signedShort, "key1", { 57 | clockTimestamp: now+shortTime, 58 | }); 59 | expect(verifiedShort.test).to.equal("test"); 60 | 61 | const verifiedMedium = await JWT.verify(signedMedium, "key2", { 62 | clockTimestamp: now+mediumTime, 63 | maxAge: JWT.age().MEDIUM, 64 | }); 65 | expect(verifiedMedium.test).to.equal("test"); 66 | 67 | const verifiedLong = await JWT.verify(signedLong, "key3", { 68 | clockTimestamp: now+longTime, 69 | maxAge: JWT.age().LONG, 70 | }); 71 | expect(verifiedLong.test).to.equal("test"); 72 | 73 | try { 74 | await JWT.verify(signedShort, "key2", {}); 75 | expect.fail("Did not throw an error"); 76 | } catch(err) { 77 | expect(err.message).to.equal("invalid signature"); 78 | } 79 | 80 | try { 81 | await JWT.verify(signedMedium, "key2", { 82 | maxAge: JWT.age().SHORT, 83 | clockTimestamp: now+mediumTime, 84 | }); 85 | expect.fail("Did not throw an error"); 86 | } catch(err) { 87 | expect(err.message).to.equal("maxAge exceeded"); 88 | } 89 | }); 90 | }); -------------------------------------------------------------------------------- /js-backend/tests/unit/utils/mail.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | 3 | const MailClass = require("../../../utils/mail.js").Mailer; 4 | const Mailer = new MailClass({}); 5 | 6 | describe("Mail (Util)", () => { 7 | it("initializes correctly", () => { 8 | expect(Mailer.transport).to.be.a("object"); 9 | expect(Mailer.emailFrom).to.be.a("string"); 10 | }); 11 | 12 | it("sends email", done => { 13 | Mailer.transport.sendMail = (data, cb) => { 14 | expect(data.hasOwnProperty("from")).to.equal(true); 15 | expect(data.hasOwnProperty("html")).to.equal(true); 16 | expect(data.html).to.equal(data.text); 17 | expect(cb).to.equal("callback"); 18 | 19 | done(); 20 | }; 21 | 22 | Mailer.sendMail({ 23 | text: "text", 24 | }, "callback"); 25 | }); 26 | }); -------------------------------------------------------------------------------- /js-backend/tests/unit/utils/password.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | const sinon = require("sinon"); 3 | 4 | process.env.ARGON2MEMORY = 1000; 5 | const { db } = require("../_shared.js"); 6 | const PwClass = require("../../../utils/password.js").PwUtil; 7 | const Pw = new PwClass(db); 8 | 9 | const passwordHash = "$argon2id$v=19$m=1000,t=5,p=1$HzqZgE7Q7CU9oDKmC9ygaA$fN2TSsxlLw3dpiSj7W4z736mx2HS//oVfnyrCbc7hUw"; // argon2 hash of "password" 10 | const httpGetStub = sinon.stub(Pw, "httpGet").resolves("cbfdac6008f9cab4083784cbd1874f76618d2a97"); // sha1 hash of "password123" 11 | 12 | describe("Password (Util)", () => { 13 | it("creates a hash", async () => { 14 | const hashed = await Pw.hashPassword("password123"); 15 | expect(hashed).to.be.a("string"); 16 | 17 | expect(await Pw.verifyPassword("password123", hashed)).to.equal(true); 18 | }); 19 | 20 | it("validates a hash", async () => { 21 | expect(await Pw.verifyPassword("password", passwordHash)).to.equal(true); 22 | expect(await Pw.verifyPassword("password123", passwordHash)).to.equal(false); 23 | }); 24 | 25 | it("checks a new password", done => { 26 | // includes pwned password check 27 | expect(httpGetStub.called).to.equal(false); 28 | 29 | Pw.checkPassword("userId", "pass").then(() => { 30 | expect.fail("Policy incompliant password passed"); 31 | }).catch(() => { 32 | Pw.checkPassword("userId", "password123").then(() => { 33 | expect.fail("Pwned passwords not checked"); 34 | }).catch(() => { 35 | expect(httpGetStub.called).to.equal(true); 36 | 37 | db.execute.callsArgWith(2, null, [{ 38 | password: "$argon2id$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$p+eSWBJTCGd8GwpCpg/sPq00obwlnQWHG06+4dd22s0", // argon2 hash of "owasp-sso-password" 39 | }]); 40 | 41 | Pw.checkPassword("userId", "owasp-sso-password").then(() => { 42 | expect.fail("Password history not checked"); 43 | }).catch(() => { 44 | expect(db.execute.called).to.equal(true); 45 | 46 | Pw.checkPassword("userId", "owasp-sso-password2").then(() => { 47 | done(); 48 | }); 49 | }); 50 | }); 51 | }); 52 | }); 53 | 54 | it("creates random strings", async () => { 55 | const tasks = []; 56 | for(let i=0;i<100;i++) { 57 | tasks.push(Pw.createRandomString(10)); 58 | } 59 | 60 | const stringList = await Promise.all(tasks); 61 | expect((new Set(stringList)).size).to.equal(100, "Duplicate strings were generated"); 62 | }); 63 | }); -------------------------------------------------------------------------------- /js-backend/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/js-backend/tmp/.gitkeep -------------------------------------------------------------------------------- /js-backend/utils/audit.js: -------------------------------------------------------------------------------- 1 | const ipCountry = require("ip-country"); 2 | const syslogPro = require("syslog-pro"); 3 | 4 | class Audit { 5 | constructor(dbConnection, ipCountrySettings) { 6 | this.db = dbConnection; 7 | 8 | ipCountry.init(ipCountrySettings); 9 | } 10 | prepareLoggers(customPages, version) { 11 | this.customPages = customPages; 12 | this.version = version; 13 | 14 | // Send hello 15 | const scheduledPromises = []; 16 | for (let websiteIndex of Object.keys(this.customPages)) { 17 | const thisPage = this.customPages[websiteIndex]; 18 | if(!thisPage.hasOwnProperty("syslog")) continue; 19 | 20 | scheduledPromises.push(this.cefSend(thisPage.syslog, { 21 | "meta": "start", 22 | })); 23 | } 24 | 25 | Promise.all(scheduledPromises).then(() => {}); 26 | 27 | // Stop previous and start new heartbeat 28 | if(this.heartbeatTimer) { 29 | clearInterval(this.heartbeatTimer); 30 | } 31 | this.heartbeatTimer = setInterval(this.heartbeat.bind(this), (process.env.SYSLOGHEARTBEAT || 60) * 1000); 32 | } 33 | add(req, object, action, attribute) { 34 | return new Promise((resolve, reject) => { 35 | const userId = req.user ? req.user.id : null; 36 | const userName = req.user && req.user.username ? req.user.username : null; 37 | const ip = this.getIP(req); 38 | const ipInfo = ipCountry.lookup(ip); 39 | const country = (ipInfo && ipInfo.country) ? ipInfo.country.iso_code : null; 40 | 41 | const loggers = []; 42 | if(this.customPages["default"].hasOwnProperty("syslog")) { 43 | loggers.push(this.customPages["default"].syslog); 44 | } 45 | if(req.ssoRequest) { 46 | const thisPage = this.customPages[req.ssoRequest.pageId.toString()]; 47 | if(thisPage.hasOwnProperty("syslog")) { 48 | loggers.push(thisPage.syslog); 49 | } 50 | } 51 | 52 | this.databaseAdd(userId, ip, country, object, action, attribute).then(auditId => { 53 | const scheduledPromises = []; 54 | loggers.forEach(syslogItem => { 55 | scheduledPromises.push(this.cefSend(syslogItem, {userName, userId, auditId, ip, country, object, action, attribute})); 56 | }); 57 | 58 | Promise.all(scheduledPromises).then(results => { 59 | resolve(auditId); 60 | }).catch(err => { 61 | console.error(err); 62 | reject(err); 63 | }); 64 | }); 65 | }); 66 | } 67 | getList(userId, offset, length) { 68 | return new Promise((resolve, reject) => { 69 | this.db.execute("SELECT * FROM audit where user = ? ORDER BY created DESC LIMIT ?, ?", [userId, offset, length], (err, results) => { 70 | if(err) return reject(err); 71 | resolve(results); 72 | }); 73 | }); 74 | } 75 | get(id) { 76 | return new Promise((resolve, reject) => { 77 | this.db.execute("SELECT * FROM audit where id = ?", [id], (err, results) => { 78 | if(err) return reject(err); 79 | resolve(results); 80 | }); 81 | }); 82 | } 83 | databaseAdd(userId, ip, country, object, action, attribute) { 84 | return new Promise((resolve, reject) => { 85 | this.db.execute("INSERT INTO audit (user, ip, country, object, action, attribute1) VALUES (?, ?, ?, ?, ?, ?)", [userId, ip, country, object, action, attribute], (err, result) => { 86 | if(err) return reject(err); 87 | resolve(result.insertId); 88 | }); 89 | }); 90 | } 91 | heartbeat() { 92 | const scheduledPromises = []; 93 | for (let websiteIndex of Object.keys(this.customPages)) { 94 | const thisPage = this.customPages[websiteIndex]; 95 | if(!thisPage.hasOwnProperty("syslog")) continue; 96 | 97 | scheduledPromises.push(this.cefSend(thisPage.syslog, { 98 | "meta": "heartbeat", 99 | })); 100 | } 101 | 102 | Promise.all(scheduledPromises).then(() => {}).catch(err => { 103 | console.error("Heartbeat failed", err); 104 | }); 105 | } 106 | cefSend(syslogDst, attributes) { 107 | const currentDate = new Date(); 108 | attributes.time = currentDate.toISOString(); 109 | const cefMessage = new syslogPro.CEF({ 110 | deviceVendor: "OWASP Foundation", 111 | deviceProduct: "OWASP SSO", 112 | deviceVersion: this.version, 113 | deviceEventClassId: "audit", 114 | name: "OWASP SSO", 115 | extensions: attributes, 116 | server: syslogDst, 117 | }); 118 | return cefMessage.send(); 119 | } 120 | getIP(req) { 121 | const forwardedFor = req.headers["x-forwarded-for"]; 122 | const clientIP = req.connection.remoteAddress; 123 | 124 | if(forwardedFor) { 125 | if(forwardedFor.indexOf(",") != -1) { 126 | return ((forwardedFor.split(","))[0]).trim(); 127 | } else { 128 | return forwardedFor; 129 | } 130 | } else { 131 | return clientIP; 132 | } 133 | } 134 | } 135 | 136 | exports.Audit = Audit; 137 | -------------------------------------------------------------------------------- /js-backend/utils/index.js: -------------------------------------------------------------------------------- 1 | const mysql = require("mysql2"); 2 | 3 | const pwUtilLib = require("./password.js").PwUtil; 4 | const UserLib = require("./user.js").User; 5 | const Mailer = require("./mail.js").Mailer; 6 | const Audit = require("./audit.js").Audit; 7 | const JWTHandler = require("./jwt.js").JWTHandler; 8 | 9 | // MOCKDB is only used by unit tests and can not be set as a normal environment variable 10 | const db = process.env.MOCKDB ? process.env.MOCKDB : mysql.createPool({ 11 | host: process.env.DBHOST, 12 | user: process.env.DBUSER, 13 | database: process.env.DBDATABASE, 14 | password: process.env.DBPASS, 15 | }); 16 | 17 | // Initiated 18 | exports.DB = db; 19 | exports.User = (new UserLib(db)); 20 | exports.PwUtil = (new pwUtilLib(db)); 21 | exports.Audit = (new Audit(db)); 22 | 23 | exports.JWT = (new JWTHandler()); 24 | 25 | const isSMTPSecure = process.env.SMTPSECURE ? (process.env.SMTPSECURE==1) : true; 26 | const MailerConfig = { 27 | host: process.env.SMTPHOST, 28 | port: process.env.SMTPPORT, 29 | secureConnection: false, // Disable SSL 30 | tls: { 31 | rejectUnauthorized: isSMTPSecure, 32 | tls: { 33 | minVersion: "TLSv1.2", 34 | }, 35 | }, 36 | }; 37 | if(process.env.SMTPUSER && process.env.SMTPPASS) { 38 | MailerConfig.auth = { 39 | user: process.env.SMTPUSER, 40 | pass: process.env.SMTPPASS, 41 | }; 42 | } 43 | exports.Mailer = (new Mailer(MailerConfig)); 44 | -------------------------------------------------------------------------------- /js-backend/utils/jwt.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | class JWTHandler { 4 | constructor() { 5 | this.hostname = process.env.DOMAIN || "localhost"; 6 | } 7 | 8 | age() { 9 | return { 10 | SHORT: "5m", 11 | MEDIUM: "1h", 12 | LONG: "1y", 13 | }; 14 | } 15 | 16 | verify(tokenData, tokenKey, options) { 17 | return new Promise((resolve, reject) => { 18 | if(!options.hasOwnProperty("issuer")) { 19 | options.issuer = this.hostname; 20 | } 21 | if(!options.hasOwnProperty("audience")) { 22 | options.audience = this.hostname; 23 | } 24 | if(!options.hasOwnProperty("maxAge")) { 25 | options.maxAge = this.age().SHORT; 26 | } 27 | 28 | try { 29 | const result = jwt.verify(tokenData, tokenKey, options); 30 | resolve(result); 31 | } catch(error) { 32 | //console.error(error); 33 | return reject(error); 34 | } 35 | }); 36 | } 37 | 38 | sign(tokenData, tokenKey, expiration) { 39 | return new Promise((resolve, reject) => { 40 | if(!tokenData.hasOwnProperty("iss")) { 41 | tokenData.iss = this.hostname; 42 | } 43 | if(!tokenData.hasOwnProperty("aud")) { 44 | tokenData.aud = this.hostname; 45 | } 46 | 47 | jwt.sign(tokenData, tokenKey, { 48 | expiresIn: expiration, 49 | }, (err, jwtData) => { 50 | if(err) return reject(err); 51 | resolve(jwtData); 52 | }); 53 | }); 54 | } 55 | } 56 | 57 | exports.JWTHandler = JWTHandler; -------------------------------------------------------------------------------- /js-backend/utils/mail.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require("nodemailer"); 2 | 3 | class Mailer { 4 | constructor(config) { 5 | this.transport = nodemailer.createTransport(config); 6 | this.transport.verify(err => { 7 | if(err) { 8 | console.error("SMTP server not available", err.message); 9 | } 10 | }); 11 | 12 | if(process.env.SMTPFROM) { 13 | this.emailFrom = process.env.SMTPFROM; 14 | } else if(process.env.SMTPUSER) { 15 | this.emailFrom = "Single Sign-On <"+process.env.SMTPUSER+">"; 16 | } else { 17 | this.emailFrom = "OWASP Single Sign-On "; 18 | } 19 | } 20 | sendMail(data, callback) { 21 | if(!data.hasOwnProperty("from")) { 22 | data.from = this.emailFrom; 23 | } 24 | if(!data.hasOwnProperty("html")) { 25 | data.html = data.text; 26 | } 27 | return this.transport.sendMail(data, callback); 28 | } 29 | } 30 | 31 | exports.Mailer = Mailer; -------------------------------------------------------------------------------- /js-backend/utils/password.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | const argon2 = require("argon2"); 3 | const https = require("https"); 4 | const url = require("url"); 5 | 6 | const isFailSafe = process.env.PWNEDPASSFAILSAFE ? (process.env.PWNEDPASSFAILSAFE==1) : false; 7 | 8 | class PwUtil { 9 | constructor(dbConnection) { 10 | this.db = dbConnection; 11 | } 12 | checkPassword(userId, password) { 13 | return new Promise((resolve, reject) => { 14 | // Check password policy 15 | if(password.length < 8 || password.length > 100) return reject("Password does not match password policy"); 16 | 17 | this.checkPwnedPasswords(password, isFailSafe).then(() => { 18 | // Check for older passwords 19 | if(userId === null || !this.db) { 20 | return resolve(); 21 | } else { 22 | this.db.execute("SELECT password FROM passwords WHERE userId = ? ORDER BY created DESC LIMIT ?", [userId, process.env.PWHISTORY || 3], (err, results) => { 23 | if(err) return reject(err); 24 | 25 | const taskList = []; 26 | for(let i=0;i { 31 | for(let i=0;i { 44 | //return resolve(); 45 | 46 | const shasum = crypto.createHash("sha1"); 47 | shasum.update(password); 48 | const shahex = shasum.digest("hex").toUpperCase(); 49 | const shaprefix = shahex.substr(0, 5); 50 | const shasuffix = shahex.substr(5); 51 | 52 | this.httpGet("https://api.pwnedpasswords.com/range/"+shahex.substr(0, 5)).then(body => { 53 | body.indexOf(shasuffix) != -1 ? reject("Password has been previously hacked and insecure") : resolve(); 54 | }).catch(err => { 55 | console.error(err, response ? response.statusCode : null); 56 | failSafe===true ? reject("Error checking pwned passwords") : resolve(); 57 | }); 58 | }); 59 | } 60 | hashPassword(password) { 61 | return argon2.hash(password, { 62 | type: process.env.ARGON2TYPE || argon2.argon2id, 63 | parallelism: process.env.ARGON2PARALLEL || 1, 64 | timeCost: process.env.ARGON2TIME || 5, 65 | memoryCost: process.env.ARGON2MEMORY || 200000, // 200 MB 66 | }); 67 | } 68 | verifyPassword(password, hash) { 69 | return argon2.verify(hash, password); 70 | } 71 | createRandomString(length) { 72 | return new Promise((resolve, reject) => { 73 | crypto.randomBytes(length, (err, buffer) => { 74 | if(err) return reject(err); 75 | resolve(buffer.toString("hex")); 76 | }); 77 | }); 78 | } 79 | httpGet(toUrl) { 80 | return new Promise((resolve, reject) => { 81 | https.get(toUrl, response => { 82 | let body = ""; 83 | response.on("data", (chunk) => body += chunk); 84 | response.on("end", () => resolve(body)); 85 | }).on("error", reject); 86 | }); 87 | } 88 | httpPost(toUrl, postData) { 89 | return new Promise((resolve, reject) => { 90 | const urlParts = url.parse(toUrl); 91 | const options = { 92 | hostname: urlParts.hostname, 93 | port: urlParts.port || 443, 94 | path: urlParts.path, 95 | method: "POST", 96 | headers: { 97 | "Content-Type": "application/json", 98 | "Content-Length": postData.length, 99 | }, 100 | }; 101 | 102 | const req = https.request(options, response => { 103 | let body = ""; 104 | response.on("data", (chunk) => body += chunk); 105 | response.on("end", () => resolve(body)); 106 | }); 107 | 108 | req.on("error", error => { 109 | reject(error); 110 | }); 111 | 112 | req.write(postData); 113 | req.end(); 114 | }); 115 | } 116 | } 117 | 118 | exports.PwUtil = PwUtil; 119 | -------------------------------------------------------------------------------- /js-backend/websites.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": { 3 | "name": "OWASP SSO", 4 | "branding": { 5 | "backgroundColor": "#f7f9fb", 6 | "fontColor": "#888", 7 | "legalName": "OWASP Foundation", 8 | "privacyPolicy": "https://owasp.org/www-policy/operational/privacy", 9 | "imprint": "https://owasp.org/contact/", 10 | "logo": "https://owasp.org/assets/images/logo.png" 11 | }, 12 | "terms": "https://owasp.org/www-policy/operational/general-disclaimer" 13 | }, 14 | "1": { 15 | "jwt": "hello-friend", 16 | "signedRequestsOnly": false, 17 | "name": "E Corp", 18 | "redirect": "https://postman-echo.com/post", 19 | "samlAllowedConsumers": [ 20 | "https://postman-echo.com/post?saml" 21 | ], 22 | "branding": { 23 | "backgroundColor": "#fff", 24 | "fontColor": "#254799", 25 | "legalName": "E Corp", 26 | "privacyPolicy": "https://www.nbcuniversal.com/privacy", 27 | "logo": "https://www.e-corp-usa.com/images/e-corp-logo-blue.png" 28 | }, 29 | "certificates": [{ 30 | "authorities": ["e-corp.ca.pem"], 31 | "webhook": { 32 | "url": "https://postman-echo.com/post", 33 | "successContains": "94:0B:DE:AD:BB:80:10:BD:17:C1:48:B4:5A:B2:66:3C:B5:75:DE:7B:89:37:65:D3:60:FF:B0:09:26:27:B2:91" 34 | } 35 | }] 36 | } 37 | } -------------------------------------------------------------------------------- /nginx/certbot-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -n "$DOMAIN" ] && [ "$DOMAIN" != "localhost" ]; then 4 | echo "Check default certificates for $DOMAIN" 5 | if [ -e "/etc/letsencrypt/live/$DOMAIN/chain.pem" ]; then 6 | echo "Certificates exist" 7 | else 8 | echo "Certificates don't exist, create one" 9 | rm -rf /etc/letsencrypt/live/$DOMAIN 10 | 11 | CERTBOTPARAMS="" 12 | if [ -n "$STAGING" ] && [ "$STAGING" == "true" ]; then 13 | echo "Using staging" 14 | CERTBOTPARAMS="--staging" 15 | fi 16 | 17 | if [ -n "$EMAIL" ]; then 18 | echo "Using own email" 19 | CERTBOTPARAMS="$CERTBOTPARAMS -m $EMAIL" 20 | else 21 | echo "Using no email" 22 | CERTBOTPARAMS="$CERTBOTPARAMS --register-unsafely-without-email" 23 | fi 24 | 25 | certbot certonly --webroot -w /var/www/certbot $CERTBOTPARAMS -d $DOMAIN --rsa-key-size 4096 --agree-tos 26 | fi 27 | else 28 | echo "No domain has been set" 29 | fi 30 | 31 | echo "Start certbot" 32 | while true; do sleep 12h; certbot renew; done; -------------------------------------------------------------------------------- /nginx/default.conf: -------------------------------------------------------------------------------- 1 | geo $trustedupstream { 2 | default 0; 3 | ${TRUSTEDPROXYIP} 1; 4 | } 5 | 6 | server { 7 | listen 80; 8 | server_name ${DOMAIN}; 9 | server_tokens off; 10 | 11 | location /.well-known/acme-challenge/ { 12 | root /var/www/certbot; 13 | } 14 | 15 | location / { 16 | return 301 https://$host$request_uri; 17 | } 18 | } 19 | 20 | server { 21 | listen 443 ssl; 22 | server_name ${DOMAIN}; 23 | server_tokens off; 24 | 25 | root /app/owasp_sso; 26 | 27 | ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; 28 | ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; 29 | include /etc/nginx/crypto/options-ssl-nginx.conf; 30 | ssl_dhparam /etc/nginx/crypto/ssl-dhparams.pem; 31 | 32 | ssl_client_certificate /etc/nginx/crypto/ca.pem; 33 | ssl_verify_client optional; 34 | 35 | #error_log /var/log/nginx/error.log debug; 36 | 37 | location / { 38 | try_files $uri $uri/ /index.html; 39 | } 40 | 41 | # Allow cert passthrough FROM a trusted proxy IP (for multiple reverse proxies) 42 | if ($trustedupstream = 1) { 43 | set $ssl_client_escaped_cert $http_x_tls_cert; 44 | set $ssl_client_verify $http_x_tls_verified; 45 | } 46 | 47 | location ^~ /api { 48 | # Basic proxy setup 49 | proxy_set_header Host $host; 50 | proxy_set_header X-Real-IP $remote_addr; 51 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 52 | 53 | # Client cert passthrough 54 | proxy_set_header X-TLS-CERT $ssl_client_escaped_cert; 55 | proxy_set_header X-TLS-VERIFIED $ssl_client_verify; 56 | 57 | proxy_ssl_session_reuse on; 58 | proxy_pass https://backend:3000; 59 | 60 | rewrite /api(.*) $1 break; 61 | } 62 | } -------------------------------------------------------------------------------- /nginx/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$DOMAIN" ]; then 4 | echo "Domain not set, using localhost" 5 | export DOMAIN="localhost" 6 | fi 7 | 8 | if [ -z "$TRUSTEDPROXYIP" ]; then 9 | echo "Trusted proxy IP not set, using localhost" 10 | export TRUSTEDPROXYIP="127.0.0.1" 11 | fi 12 | 13 | echo "Replace environment variables for $DOMAIN & $TRUSTEDPROXYIP" 14 | envsubst '${DOMAIN} ${TRUSTEDPROXYIP}' < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf 15 | #cat /etc/nginx/conf.d/default.conf 16 | 17 | echo "Check default certificates" 18 | if [ -d "/etc/letsencrypt/live/$DOMAIN" ]; then 19 | echo "Certificates exist" 20 | else 21 | echo "Certificates don't exist, using snake oil" 22 | mkdir -p /etc/letsencrypt/live/$DOMAIN 23 | #ls -l /etc/letsencrypt 24 | cp /etc/nginx/crypto/snake-fullchain.pem /etc/letsencrypt/live/$DOMAIN/fullchain.pem 25 | cp /etc/nginx/crypto/snake-privkey.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem 26 | #ls -l /etc/letsencrypt/live/$DOMAIN 27 | fi 28 | 29 | echo "Start nginx" 30 | while true; do sleep 6h; nginx -s reload; done & nginx -g "daemon off;" -------------------------------------------------------------------------------- /nginx/security.txt: -------------------------------------------------------------------------------- 1 | Contact: https://mailhide.io/e/Wno7k 2 | Acknowledgments: https://github.com/OWASP/SSO_Project/blob/master/vue-ui/src/views/about.vue#L81 3 | Preferred-Languages: en,de 4 | Policy: https://github.com/OWASP/SSO_Project/blob/master/SECURITY.md 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OWASP-SSO", 3 | "version": "1.0.0", 4 | "author": "JamesCullum (https://github.com/JamesCullum)", 5 | "license": "GPL-3.0-or-later", 6 | "description": "OWASP Single Sign-On allows a secure-by-default self-hosted SSO experience, including phishing-proof two-factor authentication, using state-of-the-art security mechanisms", 7 | "repository": "https://github.com/OWASP/SSO_Project.git", 8 | "scripts": { 9 | "cy:open": "cypress open", 10 | "cy:run": "cypress run --browser chrome", 11 | "docker:dev": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build", 12 | "docker:prod": "docker-compose -f docker-compose.yml up --build", 13 | "docker:e2e": "docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.cypress.yml up --build --exit-code-from e2e", 14 | "docker:cleanup": "docker-compose down --remove-orphans" 15 | }, 16 | "dependencies": {}, 17 | "devDependencies": { 18 | "chrome-remote-interface": "^0.28.2", 19 | "cypress": "^4.9.0", 20 | "cypress-log-to-output": "^1.0.8", 21 | "mysql2": "^2.1.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /vue-ui/.env: -------------------------------------------------------------------------------- 1 | VUE_APP_I18N_LOCALE=en 2 | VUE_APP_I18N_FALLBACK_LOCALE=en 3 | 4 | VUE_APP_BACKEND=https://localhost:3000 5 | VUE_APP_PORT=8080 -------------------------------------------------------------------------------- /vue-ui/.env.production: -------------------------------------------------------------------------------- 1 | VUE_APP_I18N_LOCALE=en 2 | VUE_APP_I18N_FALLBACK_LOCALE=en 3 | 4 | VUE_APP_BACKEND=api 5 | VUE_APP_PORT=8080 -------------------------------------------------------------------------------- /vue-ui/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /vue-ui/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "Hallo welt" 3 | } -------------------------------------------------------------------------------- /vue-ui/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": "Hello world" 3 | } -------------------------------------------------------------------------------- /vue-ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "OWASP-SSO-Frontend", 3 | "version": "1.0.0", 4 | "description": "VueJS frontend for the OWASP Single Sign-On project", 5 | "author": "JamesCullum (https://github.com/JamesCullum)", 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "test:unit": "vue-cli-service test:unit", 10 | "lint": "vue-cli-service lint", 11 | "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'", 12 | "lint:fix": "vue-cli-service lint --fix src --ext .js,.vue src" 13 | }, 14 | "dependencies": { 15 | "@vue/cli-plugin-router": "^4.4.5", 16 | "@vue/cli-service": "^4.5.13", 17 | "axios": "^0.21.3", 18 | "babel-eslint": "^10.1.0", 19 | "base64-arraybuffer": "^0.2.0", 20 | "bootstrap": "^4.5.0", 21 | "md5": "^2.2.1", 22 | "portal-vue": "^2.1.6", 23 | "sass": "^1.26.9", 24 | "sass-loader": "^8.0.0", 25 | "vee-validate": "^3.3.5", 26 | "vue": "^2.6.10", 27 | "vue-cli-plugin-i18n": "^1.0.0", 28 | "vue-country-flag": "^1.3.1", 29 | "vue-i18n": "^8.18.2", 30 | "vue-router": "^3.3.4", 31 | "vue-template-compiler": "^2.6.10" 32 | }, 33 | "devDependencies": { 34 | "@vue/cli-plugin-eslint": "^4.4.5", 35 | "@vue/cli-plugin-unit-mocha": "^4.4.5", 36 | "@vue/test-utils": "1.0.0-beta.31", 37 | "chai": "^4.1.2", 38 | "eslint": "^5.16.0", 39 | "eslint-plugin-vue": "^5.0.0", 40 | "sinon": "^9.0.2", 41 | "vue-eslint-parser": "^7.1.0" 42 | }, 43 | "eslintConfig": { 44 | "root": true, 45 | "env": { 46 | "node": true 47 | }, 48 | "extends": [ 49 | "plugin:vue/recommended" 50 | ], 51 | "rules": { 52 | "no-mixed-spaces-and-tabs": "error", 53 | "quotes": [ 54 | "error", 55 | "double", 56 | { 57 | "avoidEscape": true 58 | } 59 | ], 60 | "comma-dangle": [ 61 | "error", 62 | { 63 | "arrays": "always-multiline", 64 | "objects": "always-multiline", 65 | "imports": "never", 66 | "exports": "never", 67 | "functions": "ignore" 68 | } 69 | ], 70 | "indent": [ 71 | "error", 72 | "tab", 73 | { 74 | "SwitchCase": 1 75 | } 76 | ], 77 | "no-console": [ 78 | "error", 79 | { 80 | "allow": [ 81 | "warn", 82 | "error" 83 | ] 84 | } 85 | ], 86 | "semi": [ 87 | "error", 88 | "always" 89 | ], 90 | "no-multiple-empty-lines": [ 91 | "error", 92 | { 93 | "max": 2, 94 | "maxEOF": 1 95 | } 96 | ], 97 | "vue/html-indent": "tab", 98 | "useTabs": true, 99 | "vue/html-self-closing": [ 100 | 2, 101 | { 102 | "html": { 103 | "void": "never", 104 | "normal": "never", 105 | "component": "never" 106 | }, 107 | "svg": "always", 108 | "math": "always" 109 | } 110 | ] 111 | }, 112 | "parserOptions": { 113 | "parser": "babel-eslint" 114 | }, 115 | "overrides": [ 116 | { 117 | "files": [ 118 | "**/__tests__/*.{j,t}s?(x)" 119 | ], 120 | "env": { 121 | "mocha": true 122 | } 123 | }, 124 | { 125 | "files": [ 126 | "**/__tests__/*.{j,t}s?(x)", 127 | "**/tests/unit/**/*.spec.{j,t}s?(x)" 128 | ], 129 | "env": { 130 | "mocha": true 131 | } 132 | } 133 | ] 134 | }, 135 | "browserslist": [ 136 | "> 1%", 137 | "last 2 versions" 138 | ], 139 | "license": "GPL-3.0-or-later", 140 | "repository": { 141 | "type": "git", 142 | "url": "https://github.com/OWASP/SSO_Project.git", 143 | "directory": "vue-ui" 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /vue-ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/vue-ui/public/favicon.ico -------------------------------------------------------------------------------- /vue-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | OWASP SSO 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vue-ui/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OWASP/SSO_Project/91c612a46eaec6c291b3b1f62a5852814a5725d5/vue-ui/src/assets/logo.png -------------------------------------------------------------------------------- /vue-ui/src/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueI18n from "vue-i18n"; 3 | 4 | Vue.use(VueI18n); 5 | 6 | function loadLocaleMessages() { 7 | const locales = require.context( 8 | "./locales", 9 | true, 10 | /[A-Za-z0-9-_,\s]+\.json$/i 11 | ); 12 | const messages = {}; 13 | locales.keys().forEach(key => { 14 | const matched = key.match(/(([a-z]+)-[A-Z]+)\./i); 15 | if (matched && matched.length > 1) { 16 | const locale = locales(key); 17 | 18 | messages[matched[1]] = locale; 19 | messages[matched[2]] = locale; 20 | } 21 | }); 22 | return messages; 23 | } 24 | 25 | export default new VueI18n({ 26 | locale: navigator.language || process.env.VUE_APP_I18N_LOCALE || "en", 27 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || "en", 28 | messages: loadLocaleMessages(), 29 | }); 30 | -------------------------------------------------------------------------------- /vue-ui/src/locales/af-ZA.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/ar-SA.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "الخصوصية", 4 | "imprint": "بصمة", 5 | "about": "حول", 6 | "loading": "تحميل...", 7 | "email-address": "عنوان البريد الإلكتروني", 8 | "password": "كلمة المرور" 9 | }, 10 | "router": { 11 | "login": "تسجيل الدخول", 12 | "register": "تسجيل", 13 | "reset-password": "إعادة تعيين كلمة المرور", 14 | "change-password": "تغيير كلمة المرور", 15 | "confirm": "تأكيد هويتك", 16 | "review": "مراجعة النشاط", 17 | "about": "حول هذه الصفحة" 18 | }, 19 | "about": { 20 | "icons": "أيقونات", 21 | "opensource": "مكونات المصدر المفتوح", 22 | "security": "التقارير الأمنية", 23 | "security-banner": "لإدراجها هنا، اعثر على نقاط الضعف الأمنية الهامة وأبلغ عنها" 24 | }, 25 | "audit": { 26 | "wrong-account": "الرجاء تسجيل الدخول إلى حسابك {username} للاستمرار إلى {website}", 27 | "logout": "تسجيل الخروج", 28 | "continue-page": "المتابعة إلى {website}", 29 | "sso-out-error": "واجهت خطأ في تسجيل الدخول إلى {website}. حاول إعادة تحميل الصفحة أو محاولة تسجيل دخول آخر من هذه الصفحة!" 30 | }, 31 | "audit-log": { 32 | "load-more": "تحميل المزيد", 33 | "close-sessions": "إغلاق الجلسات", 34 | "report": "لم يكن هذا أنا - أبلغ عن حدث مشكوك فيه", 35 | "meta": "بواسطة IP {IP} في {date}", 36 | "titles": { 37 | "page-registration": "تسجيل الصفحة", 38 | "page-request": "طلب تسجيل الدخول", 39 | "page-login": "تسجيل الدخول إلى الصفحة", 40 | "login-email": "تأكيد البريد الإلكتروني", 41 | "login-password": "تسجيل دخول المستخدم", 42 | "authenticator-add": "تمت إضافة المصادقة", 43 | "authenticator-remove": "تمت إزالة المصادقة", 44 | "authenticator-login": "تأكيد المصادقة", 45 | "session-clean": "الجلسات المغلقة", 46 | "session-report": "النشاط المبلغ عنه" 47 | }, 48 | "messages": { 49 | "page-registration": "مسجل بواسطة الصفحة {attribute}", 50 | "page-request": "طلب تسجيل الدخول من {attribute}", 51 | "page-login": "تم تسجيل الدخول إلى {attribute}", 52 | "login-email": "تسجيل الدخول عبر البريد الإلكتروني تأكيد", 53 | "login-password": "تسجيل الدخول عن طريق كلمة المرور", 54 | "authenticator-add": "تم إضافة الجهاز {attribute}", 55 | "authenticator-remove": "تم إزالة الجهاز {attribute}", 56 | "authenticator-login": "تم تسجيل الدخول باستخدام {attribute}", 57 | "session-clean": "الجلسات الأخرى المغلقة", 58 | "session-report": "نشاط مشبوه مبلّغ عنه #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "إضافة مصادقة", 63 | "review": "مراجعة المصادقين", 64 | "remove": "إزالة هذه المصادقة", 65 | "hint": "هل تريد تسريع تسجيل الدخول القادم؟ أعطي هذا الجهاز اسماً وإضافته إلى حسابك.", 66 | "fido2-title": "رمز FIDO2", 67 | "cert-title": "شهادة الجهاز", 68 | "choose-type-title": "اختر نوع المصادقة التي تريد إضافتها", 69 | "choose": "اختر...", 70 | "choose-cert": "شهادة", 71 | "fill-choose": "الرجاء تحديد نوع", 72 | "label": "تسمية", 73 | "label-title": "أدخل اسم لهذا المصادق", 74 | "fill-label": "الرجاء إدخال اسم", 75 | "submit-title": "املأ التفاصيل وانقر هنا لإضافة مصادقة" 76 | }, 77 | "change-password": { 78 | "token": "رمز البريد الإلكتروني", 79 | "fill-token": "أدخل رمز التأكيد الكامل الذي تلقيته عبر البريد الإلكتروني", 80 | "confirm-password": "تأكيد كلمة المرور", 81 | "fill-confirmation": "كلمات المرور غير متطابقة", 82 | "submit": "تطبيق التغييرات" 83 | }, 84 | "login": { 85 | "404": "اسم المستخدم أو كلمة المرور غير صالحة", 86 | "fill-email": "الرجاء تقديم عنوان بريد إلكتروني صالح", 87 | "reset-password": "نسيت كلمة المرور؟", 88 | "fill-password": "سياسة كلمة المرور: الحد الأدنى 8 أحرف", 89 | "login": "تسجيل الدخول", 90 | "no-account": "ليس لديك حساب؟", 91 | "register": "تسجيل الآن", 92 | "resume": "استئناف الجلسة كـ {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "أوافق على الأحكام والشروط", 96 | "fill-toc": "تحتاج إلى الموافقة على استخدام هذه الخدمة", 97 | "success": "إذا كان عنوان البريد الإلكتروني الذي أدخلته غير مسجل بالفعل، الرجاء التحقق من البريد الوارد الخاص بك للحصول على رابط تأكيد!", 98 | "400": "لم تكن كلمة المرور الخاصة بك آمنة بما فيه الكفاية أو أنك تقوم بإعادة الضبط من شبكة مختلفة مما طلبته منها.", 99 | "register": "تسجيل", 100 | "already-account": "لديك حساب بالفعل؟", 101 | "switch-login": "العودة إلى تسجيل الدخول" 102 | }, 103 | "reset-password": { 104 | "error": "حدث خطأ - الرجاء المحاولة مرة أخرى لاحقاً!", 105 | "success": "إذا كان عنوان البريد الإلكتروني الذي أدخلته معروف، الرجاء التحقق من البريد الوارد الخاص بك للحصول على رابط إعادة تعيين كلمة المرور!", 106 | "reset-password": "إعادة تعيين كلمة المرور", 107 | "remember-password": "تذكر كلمة المرور؟" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "البريد الإلكتروني", 112 | "success": "الرجاء التحقق من البريد الوارد الخاص بك للحصول على رابط المصادقة. يمكنك إغلاق هذه النافذة الآن.", 113 | "400": "رمز البريد الإلكتروني غير صالح", 114 | "401": "مصادقة FIDO2 غير صالحة", 115 | "wrong-account": "حساب خاطئ؟" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/ca-ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/da-DK.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/el-GR.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/fi-FI.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/he-IL.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/hu-HU.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/ja-JP.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "プライバシー", 4 | "imprint": "インプリント", 5 | "about": "About", 6 | "loading": "読み込み中...", 7 | "email-address": "Emailアドレス", 8 | "password": "パスワード" 9 | }, 10 | "router": { 11 | "login": "ログイン", 12 | "register": "登録", 13 | "reset-password": "パスワードのリセット", 14 | "change-password": "パスワードの変更", 15 | "confirm": "本人確認を行う", 16 | "review": "アクティビティを確認", 17 | "about": "このページについて" 18 | }, 19 | "about": { 20 | "icons": "アイコン", 21 | "opensource": "オープンソースコンポーネント", 22 | "security": "セキュリティレポート", 23 | "security-banner": "ここにリストされるには、重要なセキュリティ脆弱性を見つけて報告してください" 24 | }, 25 | "audit": { 26 | "wrong-account": "{username} アカウントにログインして {website} に進んでください", 27 | "logout": "ログアウト", 28 | "continue-page": "{website} に進む", 29 | "sso-out-error": "{website}へのログインエラーが発生しました。ページを再読み込みするか、このページから別のサインインを試みてください!" 30 | }, 31 | "audit-log": { 32 | "load-more": "さらに読み込む", 33 | "close-sessions": "セッションを閉じる", 34 | "report": "不審なイベントを報告する", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "ページ登録", 38 | "page-request": "ログインリクエスト", 39 | "page-login": "ログインしています", 40 | "login-email": "Emailの確認", 41 | "login-password": "ユーザーログイン", 42 | "authenticator-add": "認証システムが追加されました", 43 | "authenticator-remove": "認証システムが削除されました", 44 | "authenticator-login": "認証システムの確認", 45 | "session-clean": "セッションが閉じられました", 46 | "session-report": "アクティビティの報告" 47 | }, 48 | "messages": { 49 | "page-registration": "{attribute}ページで登録", 50 | "page-request": "{attribute} からのログインリクエスト", 51 | "page-login": "{attribute} にログインしました", 52 | "login-email": "Emailでログインする", 53 | "login-password": "パスワードでログイン", 54 | "authenticator-add": "デバイス {attribute} が追加されました", 55 | "authenticator-remove": "デバイス {attribute} を削除しました", 56 | "authenticator-login": "{attribute} を使用してログインしました", 57 | "session-clean": "その他のセッションは終了しました", 58 | "session-report": "不審なアクティビティ #{attribute} を報告しました" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "認証システムを追加", 63 | "review": "認証システムの確認", 64 | "remove": "この認証システムを削除する", 65 | "hint": "次のログインをスピードアップしますか?このデバイスに名前を付けてアカウントに追加してください。", 66 | "fido2-title": "FIDO2トークン", 67 | "cert-title": "デバイス証明書", 68 | "choose-type-title": "追加する認証システムの種類を選択してください", 69 | "choose": "選択...", 70 | "choose-cert": "証明書", 71 | "fill-choose": "種類を選択してください", 72 | "label": "ラベル", 73 | "label-title": "この認証システムの名前を入力してください", 74 | "fill-label": "名前を入力してください", 75 | "submit-title": "詳細を記入し、ここをクリックして認証を追加" 76 | }, 77 | "change-password": { 78 | "token": "メールトークン", 79 | "fill-token": "メールで受け取った完全な確認トークンを入力してください", 80 | "confirm-password": "パスワードの確認", 81 | "fill-confirmation": "パスワードが一致しません", 82 | "submit": "変更を適用" 83 | }, 84 | "login": { 85 | "404": "ユーザー名またはパスワードが無効です", 86 | "fill-email": "有効なメールアドレスを入力してください", 87 | "reset-password": "パスワードをお忘れですか?", 88 | "fill-password": "パスワードポリシー:最低8文字", 89 | "login": "ログイン", 90 | "no-account": "アカウントをお持ちでないですか?", 91 | "register": "今すぐ登録", 92 | "resume": "セッションを {email} として再開" 93 | }, 94 | "register": { 95 | "toc-agree-label": "利用規約に同意します", 96 | "fill-toc": "このサービスの使用に同意する必要があります", 97 | "success": "入力したメールアドレスがまだ登録されていない場合は、メールの受信トレイに確認してください。", 98 | "400": "パスワードが十分に安全でなかったか、リクエストされたネットワークとは異なるネットワークからリセットしています。", 99 | "register": "登録", 100 | "already-account": "既にアカウントをお持ちですか?", 101 | "switch-login": "ログインに戻る" 102 | }, 103 | "reset-password": { 104 | "error": "エラーが発生しました。後でもう一度お試しください!", 105 | "success": "入力したメールアドレスがわかっている場合は、メールの受信トレイにパスワードリセットリンクがないか確認してください!", 106 | "reset-password": "パスワードのリセット", 107 | "remember-password": "パスワードを覚えていますか?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "Email", 112 | "success": "メールの受信トレイに認証リンクがないか確認してください。このウィンドウを閉じることができます。", 113 | "400": "メールトークンが無効です", 114 | "401": "無効なFIDO2認証器", 115 | "wrong-account": "間違ったアカウントですか?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/ko-KR.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/sr-SP.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/tr-TR.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/vi-VN.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "隐私", 4 | "imprint": "图形", 5 | "about": "关于", 6 | "loading": "加载中...", 7 | "email-address": "邮件地址", 8 | "password": "密码" 9 | }, 10 | "router": { 11 | "login": "登录", 12 | "register": "注册", 13 | "reset-password": "重置密码", 14 | "change-password": "更改密码", 15 | "confirm": "确认您的身份", 16 | "review": "审核活动", 17 | "about": "关于此页" 18 | }, 19 | "about": { 20 | "icons": "图标", 21 | "opensource": "开源组件", 22 | "security": "安全报告", 23 | "security-banner": "要在这里列出,找出并报告重要的安全脆弱性" 24 | }, 25 | "audit": { 26 | "wrong-account": "请登录到您的帐户 {username} 并继续到 {website}", 27 | "logout": "注销", 28 | "continue-page": "继续至 {website}", 29 | "sso-out-error": "登录到 {website}时发生错误。尝试重新加载页面或尝试从此页面登录!" 30 | }, 31 | "audit-log": { 32 | "load-more": "加载更多", 33 | "close-sessions": "关闭会话", 34 | "report": "这不是我 - 报告可疑事件", 35 | "meta": "由 IP {IP} 在 {date}", 36 | "titles": { 37 | "page-registration": "页面注册", 38 | "page-request": "登录请求", 39 | "page-login": "登录到页面", 40 | "login-email": "邮件确认", 41 | "login-password": "用户登录", 42 | "authenticator-add": "已添加身份验证器", 43 | "authenticator-remove": "身份验证器已删除", 44 | "authenticator-login": "身份验证器确认", 45 | "session-clean": "会议已关闭", 46 | "session-report": "活动已报告" 47 | }, 48 | "messages": { 49 | "page-registration": "按页面 {attribute} 注册", 50 | "page-request": "来自 {attribute} 的登录请求", 51 | "page-login": "登录到 {attribute}", 52 | "login-email": "通过邮件确认登录", 53 | "login-password": "通过密码登录", 54 | "authenticator-add": "设备 {attribute} 已添加", 55 | "authenticator-remove": "设备 {attribute} 已删除", 56 | "authenticator-login": "使用 {attribute} 登录", 57 | "session-clean": "其他会话已关闭", 58 | "session-report": "举报的可疑活动 #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "添加身份验证器", 63 | "review": "审核身份验证器", 64 | "remove": "删除此身份验证器", 65 | "hint": "您想要加速下一个登录吗?给此设备一个名称并将其添加到您的帐户。", 66 | "fido2-title": "FIDO2 令牌", 67 | "cert-title": "设备证书", 68 | "choose-type-title": "选择您想要添加的身份验证器类型", 69 | "choose": "选择...", 70 | "choose-cert": "证书", 71 | "fill-choose": "请选择类型", 72 | "label": "标签", 73 | "label-title": "输入此身份验证器的名称", 74 | "fill-label": "请输入一个名称", 75 | "submit-title": "填写详细信息并点击此处添加身份验证器" 76 | }, 77 | "change-password": { 78 | "token": "电子邮件令牌", 79 | "fill-token": "输入您通过电子邮件收到的完整确认令牌", 80 | "confirm-password": "确认密码", 81 | "fill-confirmation": "密码不匹配", 82 | "submit": "应用更改" 83 | }, 84 | "login": { 85 | "404": "用户名或密码无效", 86 | "fill-email": "请提供一个有效的电子邮件地址", 87 | "reset-password": "忘记密码?", 88 | "fill-password": "密码策略:至少 8 个字符", 89 | "login": "登录", 90 | "no-account": "没有账户?", 91 | "register": "立即注册", 92 | "resume": "恢复会话为 {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "我同意条款和条件", 96 | "fill-toc": "您需要同意使用此服务", 97 | "success": "如果您输入的电子邮件地址尚未注册,请检查您的电子邮件收件箱以获取确认链接!", 98 | "400": "您的密码不够安全,或者您正在从不同的网络重置,与您请求的网络重置。", 99 | "register": "注册", 100 | "already-account": "已经有一个帐户?", 101 | "switch-login": "返回登录" 102 | }, 103 | "reset-password": { 104 | "error": "发生错误 - 请稍后再试 !", 105 | "success": "如果您输入的电子邮件地址已经知道,请检查您的电子邮件收件箱以获取密码重置链接!", 106 | "reset-password": "重置密码", 107 | "remember-password": "记住您的密码?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "邮件地址", 112 | "success": "请检查您的电子邮件收件箱是否有认证链接。您现在可以关闭此窗口。", 113 | "400": "电子邮件令牌无效", 114 | "401": "无效的 FIDO2 验证器", 115 | "wrong-account": "账户错误?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/locales/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "privacy": "Privacy", 4 | "imprint": "Imprint", 5 | "about": "About", 6 | "loading": "Loading...", 7 | "email-address": "EMail Address", 8 | "password": "Password" 9 | }, 10 | "router": { 11 | "login": "Login", 12 | "register": "Register", 13 | "reset-password": "Reset password", 14 | "change-password": "Change password", 15 | "confirm": "Confirm your identity", 16 | "review": "Review Activity", 17 | "about": "About this page" 18 | }, 19 | "about": { 20 | "icons": "Icons", 21 | "opensource": "Open Source Components", 22 | "security": "Security Reports", 23 | "security-banner": "To be listed here, find and report important security vulnerabilities" 24 | }, 25 | "audit": { 26 | "wrong-account": "Please log into your account {username} to continue to {website}", 27 | "logout": "Log out", 28 | "continue-page": "Continue to {website}", 29 | "sso-out-error": "Encountered an error logging into {website}. Try to reload the page or attempt another sign in from this page!" 30 | }, 31 | "audit-log": { 32 | "load-more": "Load more", 33 | "close-sessions": "Close sessions", 34 | "report": "This wasn't me - report suspicious event", 35 | "meta": "by IP {IP} at {date}", 36 | "titles": { 37 | "page-registration": "Page registration", 38 | "page-request": "Login request", 39 | "page-login": "Logged into page", 40 | "login-email": "EMail confirmation", 41 | "login-password": "User login", 42 | "authenticator-add": "Authenticator added", 43 | "authenticator-remove": "Authenticator removed", 44 | "authenticator-login": "Authenticator confirmation", 45 | "session-clean": "Sessions closed", 46 | "session-report": "Activity reported" 47 | }, 48 | "messages": { 49 | "page-registration": "Registered by page {attribute}", 50 | "page-request": "Login request from {attribute}", 51 | "page-login": "Logged in to {attribute}", 52 | "login-email": "Login via EMail confirmation", 53 | "login-password": "Login via password", 54 | "authenticator-add": "Device {attribute} added", 55 | "authenticator-remove": "Device {attribute} removed", 56 | "authenticator-login": "Logged in using {attribute}", 57 | "session-clean": "Other sessions closed", 58 | "session-report": "Reported suspicious activity #{attribute}" 59 | } 60 | }, 61 | "authenticator": { 62 | "add": "Add authenticator", 63 | "review": "Review authenticators", 64 | "remove": "Remove this authenticator", 65 | "hint": "Do you want to speed up your next login? Give this device a name and add it to your account.", 66 | "fido2-title": "FIDO2 Token", 67 | "cert-title": "Device Certificate", 68 | "choose-type-title": "Choose the type of authenticator you want to add", 69 | "choose": "Choose...", 70 | "choose-cert": "Certificate", 71 | "fill-choose": "Please select a type", 72 | "label": "Label", 73 | "label-title": "Enter a name for this authenticator", 74 | "fill-label": "Please enter a name", 75 | "submit-title": "Fill out details and click here to add an authenticator" 76 | }, 77 | "change-password": { 78 | "token": "E-Mail Token", 79 | "fill-token": "Enter the full confirmation token that you received via email", 80 | "confirm-password": "Confirm Password", 81 | "fill-confirmation": "The passwords do not match", 82 | "submit": "Apply Changes" 83 | }, 84 | "login": { 85 | "404": "Username or password invalid", 86 | "fill-email": "Please provide a valid email address", 87 | "reset-password": "Forgot Password?", 88 | "fill-password": "Password policy: Minimum 8 characters", 89 | "login": "Login", 90 | "no-account": "Don't have an account?", 91 | "register": "Register Now", 92 | "resume": "Resume session as {email}" 93 | }, 94 | "register": { 95 | "toc-agree-label": "I agree to the Terms and Conditions", 96 | "fill-toc": "You need to agree to use this service", 97 | "success": "If the email address you entered is not already registered, please check your email inbox for a confirmation link!", 98 | "400": "Your password was not sufficiently secure or you are resetting from a different network than you requested it from.", 99 | "register": "Register", 100 | "already-account": "Already have an account?", 101 | "switch-login": "Back to login" 102 | }, 103 | "reset-password": { 104 | "error": "An error occured - please try again later!", 105 | "success": "If the email address you entered is known, please check your email inbox for a password reset link!", 106 | "reset-password": "Reset password", 107 | "remember-password": "Remember you password?" 108 | }, 109 | "confirm": { 110 | "fido2": "FIDO2", 111 | "email": "EMail", 112 | "success": "Please check your email inbox for an authentication link. You can close this window now.", 113 | "400": "Email token invalid", 114 | "401": "Invalid FIDO2 authenticator", 115 | "wrong-account": "Wrong account?" 116 | } 117 | } -------------------------------------------------------------------------------- /vue-ui/src/plugins/vee-validate.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | import { ValidationProvider, ValidationObserver, extend } from "vee-validate"; 4 | import { 5 | required, 6 | email, 7 | regex, 8 | numeric, 9 | min, 10 | max, 11 | confirmed 12 | } from "vee-validate/dist/rules"; 13 | 14 | extend("email", email); 15 | extend("required", required); 16 | extend("regex", regex); 17 | extend("numeric", numeric); 18 | extend("min", min); 19 | extend("max", max); 20 | extend("confirmed", confirmed); 21 | Vue.component("ValidationProvider", ValidationProvider); 22 | Vue.component("ValidationObserver", ValidationObserver); 23 | -------------------------------------------------------------------------------- /vue-ui/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import VueRouter from "vue-router"; 3 | import i18n from "../i18n"; 4 | 5 | const Register = () => import("@/views/register.vue"); 6 | const TwoFactorAuth = () => import("@/views/two-factor-auth.vue"); 7 | const FlowIn = () => import("@/views/flow-in.vue"); 8 | 9 | Vue.use(VueRouter); 10 | 11 | const routes = [ 12 | { 13 | path: "/", 14 | name: "login", 15 | component: () => import("@/views/login.vue"), 16 | meta: { 17 | title: i18n.t("router.login"), 18 | }, 19 | }, 20 | { 21 | path: "/register", 22 | name: "register", 23 | component: Register, 24 | meta: { 25 | title: i18n.t("router.register"), 26 | }, 27 | }, 28 | { 29 | path: "/register/:token", 30 | name: "register-confirm", 31 | component: Register, 32 | meta: { 33 | title: i18n.t("router.register"), 34 | }, 35 | }, 36 | { 37 | path: "/reset-password", 38 | name: "reset-password", 39 | component: () => import("@/views/reset-password.vue"), 40 | meta: { 41 | title: i18n.t("router.reset-password"), 42 | }, 43 | }, 44 | { 45 | path: "/change-password/:token", 46 | name: "change-password", 47 | component: () => import("@/views/change-password.vue"), 48 | meta: { 49 | title: i18n.t("router.change-password"), 50 | }, 51 | }, 52 | { 53 | path: "/two-factor", 54 | name: "two-factor", 55 | component: TwoFactorAuth, 56 | meta: { 57 | title: i18n.t("router.confirm"), 58 | }, 59 | }, 60 | { 61 | path: "/two-factor/:token", 62 | name: "two-factor-email-confirm", 63 | component: TwoFactorAuth, 64 | meta: { 65 | title: i18n.t("router.confirm"), 66 | }, 67 | beforeEnter: (to, from, next) => { 68 | if(from.name == "two-factor") { 69 | // Change from /two-factor to /two-factor/:id will not reload the component 70 | // and there is no good way to just have the mounted hook re-run - ideas welcome 71 | window.location.reload(); 72 | } 73 | 74 | next(); 75 | }, 76 | }, 77 | { 78 | path: "/audit", 79 | name: "audit", 80 | component: () => import("@/views/audit.vue"), 81 | meta: { 82 | title: i18n.t("router.review"), 83 | }, 84 | }, 85 | { 86 | path: "/in/:id/:data", 87 | name: "flow-in-signed", 88 | component: FlowIn, 89 | meta: { 90 | title: i18n.t("general.loading"), 91 | }, 92 | }, 93 | { 94 | path: "/in/:id", 95 | name: "flow-in", 96 | component: FlowIn, 97 | meta: { 98 | title: i18n.t("general.loading"), 99 | }, 100 | }, 101 | { 102 | path: "/about", 103 | name: "about", 104 | component: () => import("@/views/about.vue"), 105 | meta: { 106 | title: i18n.t("router.about"), 107 | }, 108 | }, 109 | ]; 110 | 111 | const router = new VueRouter({ 112 | routes, 113 | }); 114 | 115 | router.beforeEach((to, from, next) => { 116 | document.title = to.meta.title; 117 | next(); 118 | }); 119 | 120 | export default router; 121 | -------------------------------------------------------------------------------- /vue-ui/src/views/about.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 96 | 101 | -------------------------------------------------------------------------------- /vue-ui/src/views/audit.vue: -------------------------------------------------------------------------------- 1 | 96 | 168 | -------------------------------------------------------------------------------- /vue-ui/src/views/change-password.vue: -------------------------------------------------------------------------------- 1 | 100 | 101 | 135 | -------------------------------------------------------------------------------- /vue-ui/src/views/flow-in.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 53 | -------------------------------------------------------------------------------- /vue-ui/src/views/reset-password.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 88 | -------------------------------------------------------------------------------- /vue-ui/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /vue-ui/tests/unit/components/auditlog.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | import { expect } from "chai"; 3 | import sinon from "sinon"; 4 | import AuditLog from "@/components/AuditLog.vue"; 5 | 6 | const apiGet = sinon.stub(), 7 | apiPost = sinon.stub(); 8 | 9 | const page1 = [], page2 = []; 10 | for(let i=0;i<10;i++) { 11 | const dummyItem = { 12 | id: i, 13 | object: "object", 14 | action: "action", 15 | country: "country", 16 | ip: "ip", 17 | }; 18 | 19 | (i<5) ? page1.push(dummyItem) : page2.push(dummyItem); 20 | } 21 | 22 | apiGet.withArgs("/audit/logs?page=0").resolves({ 23 | data: page1, 24 | }); 25 | apiGet.withArgs("/audit/logs?page=1").resolves({ 26 | data: page2, 27 | }); 28 | apiPost.resolves({}); 29 | 30 | const wrapper = shallowMount(AuditLog, { 31 | mocks: { 32 | $t: key => key, 33 | $i18n: { 34 | locale: "en", 35 | }, 36 | }, 37 | parentComponent: { 38 | data() { 39 | return { 40 | user: { 41 | session: "session", 42 | }, 43 | }; 44 | }, 45 | methods: { 46 | apiPost, 47 | apiGet, 48 | }, 49 | }, 50 | stubs: [ 51 | "country-flag", 52 | ], 53 | }); 54 | 55 | describe("AuditLog (Component)", () => { 56 | it("has proper default values", () => { 57 | expect(wrapper.vm.$data.activeAccordion).to.equal(""); 58 | expect(wrapper.vm.$data.auditPage).to.equal(0); 59 | expect(wrapper.vm.$data.loading).to.equal(false); 60 | 61 | expect(wrapper.emitted("ready").length).to.equal(1); 62 | }); 63 | 64 | it("can expand and collapse accordion", () => { 65 | expect(wrapper.vm.$data.activeAccordion).to.equal(""); 66 | 67 | wrapper.get("button.btn").trigger("click"); 68 | expect(wrapper.vm.$data.activeAccordion).to.not.equal(""); 69 | 70 | wrapper.get("button.btn").trigger("click"); 71 | expect(wrapper.vm.$data.activeAccordion).to.equal(""); 72 | }); 73 | 74 | it("loads log entries", async () => { 75 | expect(wrapper.vm.$data.auditLogs).to.be.an("array").and.has.lengthOf(5); 76 | expect(apiGet.calledWith("/audit/logs?page=0")).to.equal(true); 77 | expect(wrapper.emitted("reload")).to.equal(undefined); 78 | 79 | wrapper.get("#loadMoreAudit").trigger("click"); 80 | await wrapper.vm.$nextTick(); 81 | 82 | expect(wrapper.vm.$data.auditLogs).to.be.an("array").and.has.lengthOf(10); 83 | expect(wrapper.emitted("reload").length).to.equal(1); 84 | }); 85 | 86 | it("closes session", async () => { 87 | apiPost.resetHistory(); 88 | 89 | wrapper.get("#closeSessions").trigger("click"); 90 | await wrapper.vm.$nextTick(); 91 | 92 | expect(apiPost.calledWith("/local/session-clean")).to.equal(true); 93 | expect(wrapper.vm.$data.auditLogs).to.be.an("array").and.has.lengthOf.most(5); 94 | }); 95 | 96 | it("reports suspicious log entry", async () => { 97 | apiPost.resetHistory(); 98 | 99 | wrapper.get(".report-log").trigger("click"); 100 | await wrapper.vm.$nextTick(); 101 | 102 | expect(apiPost.calledWith("/local/session-clean")).to.equal(true); 103 | expect(apiPost.calledWith("/audit/report")).to.equal(true); 104 | }); 105 | }); -------------------------------------------------------------------------------- /vue-ui/tests/unit/components/authenticatormanage.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import { expect } from "chai"; 3 | import sinon from "sinon"; 4 | 5 | import AuthenticatorManage from "@/components/AuthenticatorManage.vue"; 6 | import "@/plugins/vee-validate"; 7 | 8 | const apiPost = sinon.stub(), 9 | apiGet = sinon.stub(), 10 | axios = sinon.stub(), 11 | navigatorCreate = sinon.stub(), 12 | confirmStub = sinon.stub(); 13 | 14 | apiGet.resolves({ 15 | data: { 16 | token: "token", 17 | options: { 18 | challenge: "challenge", 19 | user: { 20 | id: "id", 21 | }, 22 | }, 23 | }, 24 | }); 25 | apiPost.resolves({}); 26 | 27 | const baseOptions = { 28 | data() { 29 | return { 30 | fidoAvailable: true, 31 | }; 32 | }, 33 | mocks: { 34 | $t: key => key, 35 | axios, 36 | }, 37 | parentComponent: { 38 | data() { 39 | return { 40 | user: { 41 | session: "session", 42 | username: "username", 43 | authenticators: [{ 44 | handle: "handle", 45 | userHandle: "userHandle", 46 | type: "fido2", 47 | label: "label", 48 | }], 49 | }, 50 | backend: "backend", 51 | }; 52 | }, 53 | methods: { 54 | apiPost, 55 | apiGet, 56 | }, 57 | }, 58 | }; 59 | const wrapper = mount(AuthenticatorManage, baseOptions); 60 | 61 | describe("AuthenticatorManage (Component)", () => { 62 | it("can delete an authenticator", async () => { 63 | expect(apiPost.called).to.equal(false); 64 | expect(confirmStub.called).to.equal(false); 65 | expect(wrapper.emitted("reload")).to.equal(undefined); 66 | 67 | confirmStub.returns(true); 68 | global.confirm = confirmStub; 69 | 70 | wrapper.get("a.remove-authenticator").trigger("click"); 71 | await wrapper.vm.$nextTick(); 72 | 73 | expect(confirmStub.called).to.equal(true); 74 | expect(apiPost.calledWith("/authenticator/delete")).to.equal(true); 75 | expect(wrapper.emitted("reload").length).to.equal(1); 76 | }); 77 | 78 | it("creates a certificate", done => { 79 | expect(axios.called).to.equal(false); 80 | 81 | const createObjectURL = sinon.stub(); 82 | createObjectURL.returns("createObjectURL"); 83 | window.URL.createObjectURL = createObjectURL; 84 | 85 | axios.resolves({ 86 | data: { 87 | data: "data", 88 | }, 89 | }); 90 | 91 | wrapper.setData({ 92 | createType: "cert", 93 | createLabel: "createLabel", 94 | }); 95 | wrapper.vm.$nextTick().then(() => { 96 | const beforeCalls = wrapper.emitted("reload") ? wrapper.emitted("reload").length : 0; 97 | 98 | wrapper.get("form").trigger("submit"); 99 | setTimeout(() => { 100 | expect(axios.called).to.equal(true); 101 | expect(createObjectURL.called).to.equal(true); 102 | expect(wrapper.emitted("reload").length).to.equal(beforeCalls+1); 103 | 104 | done(); 105 | }, 10); 106 | }); 107 | }); 108 | 109 | it("creates a fido2 authenticator", done => { 110 | expect(apiGet.called).to.equal(false); 111 | expect(navigatorCreate.called).to.equal(false); 112 | expect(apiPost.calledWith("/fido2/register")).to.equal(false); 113 | 114 | wrapper.setData({ 115 | createType: "fido", 116 | createLabel: "createLabel", 117 | }); 118 | 119 | navigatorCreate.resolves({ 120 | id: "id", 121 | rawId: "rawId", 122 | response: { 123 | clientDataJSON: "clientDataJSON", 124 | attestationObject: "attestationObject", 125 | }, 126 | type: "fido2", 127 | }); 128 | delete navigator.credentials; 129 | navigator.credentials = { 130 | create: navigatorCreate, 131 | }; 132 | 133 | wrapper.vm.$nextTick().then(() => { 134 | const beforeCalls = wrapper.emitted("reload") ? wrapper.emitted("reload").length : 0; 135 | 136 | wrapper.get("form").trigger("submit"); 137 | setTimeout(() => { 138 | expect(apiGet.calledWith("/fido2/register")).to.equal(true); 139 | expect(navigatorCreate.called).to.equal(true); 140 | expect(apiPost.calledWith("/fido2/register")).to.equal(true); 141 | expect(wrapper.emitted("reload").length).to.equal(beforeCalls+1); 142 | 143 | done(); 144 | }, 10); 145 | }); 146 | }); 147 | }); -------------------------------------------------------------------------------- /vue-ui/tests/unit/views/about.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | import { expect } from "chai"; 3 | import About from "@/views/about.vue"; 4 | 5 | const wrapper = shallowMount(About, { 6 | mocks: { 7 | $t: key => key, 8 | }, 9 | }); 10 | 11 | describe("About (View)", () => { 12 | it("has proper default values", () => { 13 | expect(wrapper.vm.$data.activeAccordion).to.equal(""); 14 | expect(wrapper.vm.$data.categories).to.be.an("array").and.has.lengthOf.at.least(3); 15 | 16 | const firstItem = wrapper.vm.$data.categories[0]; 17 | expect(firstItem).to.have.property("name"); 18 | expect(firstItem).to.have.property("list"); 19 | expect(firstItem.name).to.equal("about.icons"); 20 | 21 | expect(firstItem.list).to.be.an("array").to.have.lengthOf.at.least(3); 22 | }); 23 | 24 | it("can expand and collapse accordion", () => { 25 | expect(wrapper.vm.$data.activeAccordion).to.equal(""); 26 | 27 | wrapper.get("button.btn").trigger("click"); 28 | expect(wrapper.vm.$data.activeAccordion).to.not.be.equal(""); 29 | 30 | wrapper.get("button.btn").trigger("click"); 31 | expect(wrapper.vm.$data.activeAccordion).to.be.equal(""); 32 | }); 33 | 34 | it("displays title, badge and list", async () => { 35 | const instanceData = wrapper.vm.$data; 36 | 37 | let securityIndex = false; 38 | for(let i=0;i key, 13 | }, 14 | stubs: [ 15 | "AuditLog", 16 | "AuthenticatorManage", 17 | ], 18 | parentComponent: { 19 | methods: { 20 | getMe, 21 | logout, 22 | apiPost, 23 | }, 24 | }, 25 | }; 26 | const wrapper = shallowMount(Audit, baseOptions); 27 | 28 | baseOptions.parentComponent.data = () => { 29 | return { 30 | ssoPage: { 31 | name: "Test", 32 | pageId: 1, 33 | flowType: "jwt", 34 | }, 35 | }; 36 | }; 37 | const ssoWrapper = shallowMount(Audit, baseOptions); 38 | 39 | baseOptions.parentComponent.data = () => { 40 | return { 41 | ssoPage: { 42 | name: "Test", 43 | pageId: 1, 44 | flowType: "saml", 45 | }, 46 | }; 47 | }; 48 | const ssoSAMLWrapper = shallowMount(Audit, baseOptions); 49 | 50 | describe("Audit (View)", () => { 51 | it("loads the user on mount", () => { 52 | expect(getMe.called).to.equal(true); 53 | expect(wrapper.vm.$data.loading).to.equal(false); 54 | }); 55 | 56 | it("calls logout", () => { 57 | expect(logout.called).to.equal(false); 58 | wrapper.get("button.btn-warning").trigger("click"); 59 | expect(logout.called).to.equal(true); 60 | }); 61 | 62 | it("only shows SSO login button if valid", () => { 63 | expect(wrapper.find("button.btn-primary.float-right").exists()).to.equal(false); 64 | expect(ssoWrapper.find("button.btn-primary.float-right").exists()).to.equal(true); 65 | }); 66 | 67 | it("JWT flow works", done => { 68 | apiPost.resetHistory(); 69 | expect(apiPost.called).to.equal(false); 70 | expect(ssoWrapper.vm.$data.jwtToken).to.equal(""); 71 | 72 | apiPost.resolves({ 73 | data: { 74 | redirect: "http://example.com", 75 | token: "token", 76 | }, 77 | }); 78 | ssoWrapper.get("button.btn-primary.float-right").trigger("click"); 79 | 80 | window.HTMLFormElement.prototype.submit = () => { 81 | done(); 82 | }; 83 | 84 | ssoWrapper.vm.$nextTick().then(() => { 85 | expect(apiPost.called).to.equal(true); 86 | expect(apiPost.calledWith("/flow/out")).to.equal(true); 87 | 88 | expect(ssoWrapper.vm.$data.redirect).to.equal("http://example.com"); 89 | expect(ssoWrapper.vm.$data.jwtToken).to.equal("token"); 90 | expect(ssoWrapper.vm.$data.SAMLResponse).to.equal(""); 91 | expect(ssoWrapper.vm.$data.RelayState).to.equal(""); 92 | }); 93 | }); 94 | 95 | it("SAML flow works", done => { 96 | apiPost.resetHistory(); 97 | expect(apiPost.called).to.equal(false); 98 | expect(ssoSAMLWrapper.vm.$data.SAMLResponse).to.equal(""); 99 | 100 | apiPost.resolves({ 101 | data: { 102 | redirect: "http://example.com", 103 | SAMLResponse: "SAMLResponse", 104 | RelayState: "RelayState", 105 | }, 106 | }); 107 | ssoSAMLWrapper.get("button.btn-primary.float-right").trigger("click"); 108 | 109 | window.HTMLFormElement.prototype.submit = () => { 110 | done(); 111 | }; 112 | 113 | ssoSAMLWrapper.vm.$nextTick().then(() => { 114 | expect(apiPost.calledWith("/flow/out")).to.equal(true); 115 | 116 | expect(ssoSAMLWrapper.vm.$data.jwtToken).to.equal(""); 117 | expect(ssoSAMLWrapper.vm.$data.SAMLResponse).to.equal("SAMLResponse"); 118 | expect(ssoSAMLWrapper.vm.$data.RelayState).to.equal("RelayState"); 119 | }); 120 | }); 121 | }); -------------------------------------------------------------------------------- /vue-ui/tests/unit/views/change-password.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import { expect } from "chai"; 3 | import sinon from "sinon"; 4 | 5 | import ChangePassword from "@/views/change-password.vue"; 6 | import "@/plugins/vee-validate"; 7 | 8 | const setLoginToken = sinon.spy(), 9 | routerPush = sinon.spy(), 10 | apiPost = sinon.stub(); 11 | 12 | const baseOptions = { 13 | mocks: { 14 | $t: key => key, 15 | $route: { 16 | params: { 17 | token: "12345", 18 | }, 19 | }, 20 | $router: { 21 | push: routerPush, 22 | }, 23 | }, 24 | parentComponent: { 25 | methods: { 26 | setLoginToken, 27 | apiPost, 28 | }, 29 | }, 30 | }; 31 | const wrapper = mount(ChangePassword, baseOptions); 32 | 33 | const longToken = "123456789012345678901234567890123456789012345678901234567890"; 34 | baseOptions.mocks.$route.params.token = longToken; 35 | baseOptions.data = () => { 36 | return { 37 | password: "password", 38 | confirm: "password", 39 | }; 40 | }; 41 | const tokenWrapper = mount(ChangePassword, baseOptions); 42 | 43 | describe("Change Password (View)", () => { 44 | it("has proper default values", () => { 45 | expect(wrapper.vm.$data.token).to.equal("12345"); 46 | expect(tokenWrapper.vm.$data.token).to.equal(longToken); 47 | }); 48 | 49 | it("allows the token to be entered if it is too short", () => { 50 | expect(wrapper.vm.tokenFromUrl).to.equal(false); 51 | expect(tokenWrapper.vm.tokenFromUrl).to.equal(true); 52 | 53 | expect(wrapper.get("#token").attributes().hasOwnProperty("disabled")).to.equal(false); 54 | expect(tokenWrapper.get("#token").attributes().hasOwnProperty("disabled")).to.equal(true); 55 | }); 56 | 57 | it("disabled the token when it is complete", async () => { 58 | expect(wrapper.vm.$data.token).to.equal("12345"); 59 | expect(wrapper.vm.tokenFromUrl).to.equal(false); 60 | 61 | wrapper.setData({ 62 | token: longToken, 63 | }); 64 | await wrapper.vm.$nextTick(); 65 | 66 | expect(wrapper.vm.$data.token).to.equal(longToken); 67 | expect(wrapper.vm.tokenFromUrl).to.equal(true); 68 | }); 69 | 70 | it("delegates a login to $root", async () => { 71 | expect(apiPost.called).to.equal(false); 72 | expect(routerPush.called).to.equal(false); 73 | expect(setLoginToken.called).to.equal(false); 74 | 75 | apiPost.resolves({ 76 | data: { 77 | username: "username", 78 | side: "side", 79 | }, 80 | }); 81 | 82 | tokenWrapper.get("form").trigger("submit"); 83 | await tokenWrapper.vm.$nextTick(); 84 | 85 | expect(apiPost.calledWith("/local/change")).to.equal(true); 86 | expect(routerPush.calledWith("/audit")).to.equal(true); 87 | expect(setLoginToken.calledWith("username")).to.equal(true); 88 | }); 89 | }); -------------------------------------------------------------------------------- /vue-ui/tests/unit/views/flow-in.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | import { expect } from "chai"; 3 | import sinon from "sinon"; 4 | 5 | import FlowIn from "@/views/flow-in.vue"; 6 | 7 | const responseToken = { 8 | data: { 9 | username: "username", 10 | page: { 11 | id: 1, 12 | }, 13 | }, 14 | }; 15 | 16 | const apiPost = sinon.stub(), 17 | apiGet = sinon.stub(); 18 | apiPost.resolves(responseToken); 19 | apiGet.resolves(responseToken); 20 | 21 | const baseOptions = { 22 | mocks: { 23 | $t: key => key, 24 | }, 25 | parentComponent: { 26 | methods: { 27 | apiPost, 28 | apiGet, 29 | }, 30 | }, 31 | }; 32 | 33 | describe("Flow-in (View)", () => { 34 | it("processes SAML correctly", async () => { 35 | const setLoginTokenSAML = sinon.spy(), 36 | localStorageSetSAML = sinon.stub(), 37 | redirectSAML = sinon.spy(); 38 | 39 | baseOptions.mocks.$route = { 40 | params: { 41 | id: "saml", 42 | }, 43 | query: { 44 | SAMLRequest: "SAMLRequest", 45 | RelayState: "RelayState", 46 | }, 47 | }; 48 | baseOptions.parentComponent.methods.setLoginToken = setLoginTokenSAML; 49 | global.localStorage = { 50 | setItem: localStorageSetSAML, 51 | }; 52 | delete window.location; 53 | window.location = { 54 | reload: redirectSAML, 55 | }; 56 | const samlWrapper = shallowMount(FlowIn, baseOptions); 57 | 58 | await samlWrapper.vm.$nextTick(); 59 | 60 | expect(apiGet.calledWith("/saml")).to.equal(true); 61 | expect(setLoginTokenSAML.calledWith("username")).to.equal(true); 62 | expect(localStorageSetSAML.calledWith("sso-request")).to.equal(true); 63 | expect(redirectSAML.called).to.equal(true); 64 | }); 65 | 66 | it("processes JWT correctly", async () => { 67 | const setLoginTokenJWT = sinon.spy(), 68 | localStorageSetJWT = sinon.stub(), 69 | redirectJWT = sinon.spy(); 70 | 71 | baseOptions.mocks.$route = { 72 | params: { 73 | id: "1", 74 | data: "data", 75 | }, 76 | }; 77 | baseOptions.parentComponent.methods.setLoginToken = setLoginTokenJWT; 78 | global.localStorage = { 79 | setItem: localStorageSetJWT, 80 | }; 81 | delete window.location; 82 | window.location = { 83 | reload: redirectJWT, 84 | }; 85 | const jwtWrapper = shallowMount(FlowIn, baseOptions); 86 | 87 | await jwtWrapper.vm.$nextTick(); 88 | 89 | expect(apiPost.calledWith("/flow/in")).to.equal(true); 90 | expect(setLoginTokenJWT.calledWith("username")).to.equal(true); 91 | expect(localStorageSetJWT.calledWith("sso-request")).to.equal(true); 92 | expect(redirectJWT.called).to.equal(true); 93 | }); 94 | }); -------------------------------------------------------------------------------- /vue-ui/tests/unit/views/login.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import { expect } from "chai"; 3 | import sinon from "sinon"; 4 | 5 | import Login from "@/views/login.vue"; 6 | import "@/plugins/vee-validate"; 7 | 8 | const setLoginToken = sinon.spy(), 9 | routerPush = sinon.spy(), 10 | apiPost = sinon.stub(), 11 | getMe = sinon.stub(), 12 | useLoginToken = sinon.spy(), 13 | listLoginToken = sinon.stub(); 14 | 15 | apiPost.resolves({ 16 | data: { 17 | username: "username", 18 | test: "test", 19 | }, 20 | }); 21 | getMe.resolves({}); 22 | listLoginToken.returns(["email1", "email2"]); 23 | 24 | const baseOptions = { 25 | data() { 26 | return { 27 | email: "email", 28 | password: "password", 29 | certSubmitted: true, 30 | }; 31 | }, 32 | mocks: { 33 | $t: key => key, 34 | $route: { 35 | params: { 36 | token: "12345", 37 | }, 38 | }, 39 | $router: { 40 | push: routerPush, 41 | }, 42 | }, 43 | stubs: [ 44 | "router-link", 45 | ], 46 | parentComponent: { 47 | methods: { 48 | listLoginToken, 49 | useLoginToken, 50 | setLoginToken, 51 | apiPost, 52 | getMe, 53 | }, 54 | }, 55 | }; 56 | const wrapper = mount(Login, baseOptions); 57 | 58 | baseOptions.parentComponent.data = () => { 59 | return { 60 | authToken: "authToken", 61 | user: { 62 | isAuthenticated: false, 63 | id: 1, 64 | }, 65 | }; 66 | }; 67 | 68 | describe("Login (View)", () => { 69 | it("only shows login to guest users", () => { 70 | expect(getMe.called).to.equal(false); 71 | expect(apiPost.called).to.equal(false); 72 | expect(wrapper.vm.$data.loading).to.equal(false); 73 | expect(listLoginToken.called).to.equal(true); 74 | }); 75 | 76 | it("can resume a logged in session", async () => { 77 | expect(useLoginToken.called).to.equal(false); 78 | 79 | wrapper.get("#resume-session a").trigger("click"); 80 | await wrapper.vm.$nextTick(); 81 | expect(useLoginToken.calledWith("email1")).to.equal(true); 82 | }); 83 | 84 | it("forwards login attempts", async () => { 85 | expect(apiPost.called).to.equal(false); 86 | expect(setLoginToken.called).to.equal(false); 87 | 88 | wrapper.get("form").trigger("submit"); 89 | await wrapper.vm.$nextTick(); 90 | 91 | expect(apiPost.calledWith("/local/login")).to.equal(true); 92 | expect(setLoginToken.calledWith("username")).to.equal(true); 93 | expect(wrapper.vm.$data.loading).to.equal(false); 94 | // The routeUser routine doesn't get called, because we can not properly change the $root.authToken as reaction to setLoginToken 95 | // This is why we need to test it in two parts 96 | expect(getMe.called).to.equal(false); 97 | }); 98 | 99 | it("attempts a cert login after a successful login", done => { 100 | expect(getMe.called).to.equal(false); 101 | 102 | window.HTMLFormElement.prototype.submit = () => { 103 | expect(getMe.called).to.equal(true); 104 | 105 | done(); 106 | }; 107 | 108 | const loggedinWrapper = mount(Login, baseOptions); 109 | loggedinWrapper.vm.$nextTick().then(() => { 110 | loggedinWrapper.vm.routeUser(); 111 | }); 112 | }); 113 | 114 | it("falls back to two-factor if certificate times out", done => { 115 | routerPush.resetHistory(); 116 | window.HTMLFormElement.prototype.submit = () => {}; 117 | 118 | const loggedinWrapper = mount(Login, baseOptions); 119 | loggedinWrapper.vm.$nextTick().then(() => { 120 | loggedinWrapper.vm.certFrameLoad(); 121 | setTimeout(() => { 122 | expect(routerPush.calledWith("/two-factor")).to.equal(true); 123 | 124 | done(); 125 | }, 1500); 126 | }); 127 | }); 128 | 129 | it("sets the login token on success", done => { 130 | setLoginToken.resetHistory(); 131 | routerPush.resetHistory(); 132 | expect(setLoginToken.called).to.equal(false); 133 | expect(routerPush.called).to.equal(false); 134 | 135 | window.postMessage({ 136 | username: "username", 137 | token: "token", 138 | }, "*"); 139 | 140 | setTimeout(() => { 141 | expect(setLoginToken.calledWith("username")).to.equal(true); 142 | expect(routerPush.calledWith("/audit")).to.equal(true); 143 | 144 | done(); 145 | }, 10); 146 | }); 147 | }); -------------------------------------------------------------------------------- /vue-ui/tests/unit/views/register.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import { expect } from "chai"; 3 | import sinon from "sinon"; 4 | 5 | import Register from "@/views/register.vue"; 6 | import "@/plugins/vee-validate"; 7 | 8 | const setLoginToken = sinon.spy(), 9 | routerPush = sinon.spy(), 10 | apiPost = sinon.stub(); 11 | 12 | apiPost.resolves({ 13 | data: { 14 | username: "username", 15 | test: "test", 16 | }, 17 | }); 18 | 19 | const baseOptions = { 20 | data() { 21 | return { 22 | email: "email", 23 | password: "password", 24 | }; 25 | }, 26 | mocks: { 27 | $t: key => key, 28 | $i18n: { 29 | locale: "en", 30 | fallbackLocale: "en", 31 | }, 32 | $route: { 33 | params: { 34 | token: "", 35 | }, 36 | }, 37 | $router: { 38 | push: routerPush, 39 | }, 40 | }, 41 | stubs: [ 42 | "router-link", 43 | ], 44 | parentComponent: { 45 | data() { 46 | return { 47 | ssoPage: {}, 48 | defaultPage: { 49 | terms: "http://example.com", 50 | }, 51 | }; 52 | }, 53 | methods: { 54 | setLoginToken, 55 | apiPost, 56 | }, 57 | }, 58 | }; 59 | const wrapper = mount(Register, baseOptions); 60 | 61 | baseOptions.mocks.$route.params = { 62 | token: "12345", 63 | }; 64 | const activateWrapper = mount(Register, baseOptions); 65 | 66 | describe("Register (View)", () => { 67 | it("loads proper default values", () => { 68 | expect(wrapper.vm.$data.token).to.equal(""); 69 | expect(wrapper.vm.$data.error).to.equal(0); 70 | expect(wrapper.vm.$data.agreeToC).to.equal(false); 71 | expect(wrapper.vm.$data.success).to.equal(null); 72 | 73 | expect(activateWrapper.vm.$data.token).to.equal("12345"); 74 | }); 75 | 76 | it("forwards registration request", async () => { 77 | expect(apiPost.calledWith("/local/register")).to.equal(false); 78 | 79 | wrapper.vm.submit(); 80 | await wrapper.vm.$nextTick(); 81 | 82 | expect(apiPost.calledWith("/local/register")).to.equal(true); 83 | expect(wrapper.vm.$data.success).to.equal(true); 84 | }); 85 | 86 | it("forwards activation request", async () => { 87 | expect(apiPost.calledWith("/local/activate")).to.equal(false); 88 | 89 | activateWrapper.vm.submit(); 90 | await activateWrapper.vm.$nextTick(); 91 | 92 | expect(apiPost.calledWith("/local/activate")).to.equal(true); 93 | expect(setLoginToken.calledWith("username")).to.equal(true); 94 | expect(routerPush.calledWith("/audit")).to.equal(true); 95 | }); 96 | }); -------------------------------------------------------------------------------- /vue-ui/tests/unit/views/reset-password.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import { expect } from "chai"; 3 | import sinon from "sinon"; 4 | 5 | import ResetPassword from "@/views/reset-password.vue"; 6 | import "@/plugins/vee-validate"; 7 | 8 | const apiPost = sinon.stub(); 9 | apiPost.resolves({}); 10 | 11 | const baseOptions = { 12 | data() { 13 | return { 14 | email: "email", 15 | }; 16 | }, 17 | mocks: { 18 | $t: key => key, 19 | }, 20 | stubs: [ 21 | "router-link", 22 | ], 23 | parentComponent: { 24 | methods: { 25 | apiPost, 26 | }, 27 | }, 28 | }; 29 | const wrapper = mount(ResetPassword, baseOptions); 30 | 31 | describe("Reset Password (View)", () => { 32 | it("forwards reset request", async () => { 33 | expect(wrapper.vm.success).to.equal(null); 34 | expect(apiPost.called).to.equal(false); 35 | 36 | wrapper.vm.submit(); 37 | await wrapper.vm.$nextTick(); 38 | 39 | expect(wrapper.vm.success).to.equal(true); 40 | expect(apiPost.calledWith("/local/change-request")).to.equal(true); 41 | }); 42 | }); -------------------------------------------------------------------------------- /vue-ui/tests/unit/views/two-factor-auth.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from "@vue/test-utils"; 2 | import { expect } from "chai"; 3 | import sinon from "sinon"; 4 | import TwoFA from "@/views/two-factor-auth.vue"; 5 | 6 | const setLoginToken = sinon.spy(), 7 | getMe = sinon.stub(), 8 | logout = sinon.spy(), 9 | apiPost = sinon.stub(), 10 | apiGet = sinon.stub(), 11 | routerPush = sinon.spy(); 12 | 13 | getMe.resolves({}); 14 | apiGet.withArgs("/email-confirm").resolves({ 15 | data: { 16 | username: "username", 17 | test: "test", 18 | }, 19 | }); 20 | apiPost.resolves({ 21 | data: { 22 | username: "username", 23 | test: "test", 24 | }, 25 | }); 26 | 27 | const baseOptions = { 28 | data() { 29 | return { 30 | fidoAvailable: true, 31 | }; 32 | }, 33 | mocks: { 34 | $t: key => key, 35 | $router: { 36 | push: routerPush, 37 | }, 38 | }, 39 | parentComponent: { 40 | data() { 41 | return { 42 | user: { 43 | id: 1, 44 | isAuthenticated: false, 45 | authenticators: [{ 46 | type: "fido2", 47 | }], 48 | }, 49 | }; 50 | }, 51 | methods: { 52 | getMe, 53 | logout, 54 | apiPost, 55 | apiGet, 56 | setLoginToken, 57 | }, 58 | }, 59 | }; 60 | // Has all things set, but user is unknown and needs to be loaded 61 | const wrapper = shallowMount(TwoFA, baseOptions); 62 | 63 | describe("Two-Factor-Auth (View)", () => { 64 | it("calls logout", async () => { 65 | logout.resetHistory(); 66 | 67 | expect(logout.called).to.equal(false); 68 | wrapper.get("#logout").trigger("click"); 69 | await wrapper.vm.$nextTick(); 70 | 71 | expect(logout.called).to.equal(true); 72 | }); 73 | 74 | it("redirects authenticated users", async () => { 75 | routerPush.resetHistory(); 76 | 77 | baseOptions.parentComponent.data = () => { 78 | return { 79 | fidoAvailable: true, 80 | user: { 81 | isAuthenticated: true, 82 | authenticators: [], 83 | }, 84 | }; 85 | }; 86 | const redirectWrapper = shallowMount(TwoFA, baseOptions); 87 | await redirectWrapper.vm.$nextTick(); 88 | 89 | expect(routerPush.calledWith("/audit")).to.equal(true); 90 | }); 91 | 92 | it("requests email confirmation", async () => { 93 | expect(wrapper.vm.$data.emailClicked).to.equal(false); 94 | expect(apiGet.calledWith("/local/email-auth")).to.equal(false); 95 | 96 | apiGet.withArgs("/local/email-auth").resolves({}); 97 | wrapper.get("#confirmEmail").trigger("click"); 98 | await wrapper.vm.$nextTick(); 99 | 100 | expect(wrapper.vm.$data.emailClicked).to.equal(true); 101 | expect(apiGet.calledWith("/local/email-auth")).to.equal(true); 102 | }); 103 | 104 | it("automatically activates email token", async () => { 105 | apiGet.resetHistory(); 106 | setLoginToken.resetHistory(); 107 | routerPush.resetHistory(); 108 | 109 | // EMail confirmation 110 | baseOptions.mocks.$route = { 111 | params: { 112 | token: "12345", 113 | }, 114 | }; 115 | baseOptions.parentComponent.data = () => { 116 | return { 117 | user: { 118 | isAuthenticated: false, 119 | authenticators: [{ 120 | type: "fido2", 121 | }], 122 | }, 123 | }; 124 | }; 125 | const confirmWrapper = shallowMount(TwoFA, baseOptions); 126 | await confirmWrapper.vm.$nextTick(); 127 | 128 | expect(apiGet.calledWith("/email-confirm")).to.equal(true); 129 | expect(setLoginToken.calledWith("username")).to.equal(true); 130 | expect(routerPush.calledWith("/audit")).to.equal(true); 131 | }); 132 | 133 | it("requests fido token", async () => { 134 | setLoginToken.resetHistory(); 135 | routerPush.resetHistory(); 136 | expect(apiGet.calledWith("/fido2/login")).to.equal(false); 137 | expect(apiPost.called).to.equal(false); 138 | 139 | apiGet.withArgs("/fido2/login").resolves({ 140 | data: { 141 | token: "token", 142 | options: { 143 | challenge: "challenge", 144 | allowCredentials: [{ 145 | id: "id", 146 | }], 147 | }, 148 | }, 149 | }); 150 | 151 | const navigatorGet = sinon.stub(); 152 | navigatorGet.resolves({ 153 | id: "id", 154 | rawId: "rawId", 155 | response: { 156 | clientDataJSON: "clientDataJSON", 157 | authenticatorData: "authenticatorData", 158 | signature: "signature", 159 | }, 160 | type: "type", 161 | }); 162 | 163 | delete navigator.credentials; 164 | navigator.credentials = { 165 | get: navigatorGet, 166 | }; 167 | 168 | wrapper.get("#confirmFido").trigger("click"); 169 | await wrapper.vm.$nextTick(); 170 | 171 | expect(navigatorGet.called).to.equal(true); 172 | expect(apiGet.calledWith("/fido2/login")).to.equal(true); 173 | 174 | await wrapper.vm.$nextTick(); 175 | expect(apiPost.calledWith("/fido2/login")).to.equal(true); 176 | 177 | await wrapper.vm.$nextTick(); 178 | expect(setLoginToken.calledWith("username")).to.equal(true); 179 | expect(routerPush.calledWith("/audit")).to.equal(true); 180 | }); 181 | }); -------------------------------------------------------------------------------- /vue-ui/vue.config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | module.exports = { 4 | pluginOptions: { 5 | i18n: { 6 | locale: process.env.VUE_APP_I18N_LOCALE || "en", 7 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || "en", 8 | localeDir: "locales", 9 | enableInSFC: false, 10 | }, 11 | }, 12 | devServer: { 13 | allowedHosts: [ 14 | "localhost", 15 | ], 16 | https: { 17 | https: true, 18 | }, 19 | public: "https://localhost:" + process.env.VUE_APP_PORT, 20 | }, 21 | }; 22 | --------------------------------------------------------------------------------