├── .editorconfig ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── Bug Report.yml │ ├── Feature Request.yml │ └── config.yml ├── actions │ ├── build │ │ └── action.yml │ ├── get-prerelease │ │ └── action.yml │ ├── get-release-notes │ │ └── action.yml │ ├── get-version │ │ └── action.yml │ ├── npm-publish │ │ └── action.yml │ ├── release-create │ │ └── action.yml │ ├── rl-scanner │ │ └── action.yml │ └── tag-exists │ │ └── action.yml ├── dependabot.yml ├── stale.yml └── workflows │ ├── browserstack.yml │ ├── codeql.yml │ ├── npm-release.yml │ ├── publish.yml │ ├── release.yml │ ├── rl-secure.yml │ ├── semgrep.yml │ ├── snyk.yml │ └── test.yml ├── .gitignore ├── .semgrepignore ├── .shiprc ├── .version ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── EXAMPLES.md ├── FAQ.md ├── LICENSE ├── MIGRATION_GUIDE.md ├── README.md ├── angular.json ├── api-server.js ├── browserstack.json ├── codecov.yml ├── cypress.json ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── main.js │ ├── navigation.js │ ├── search.js │ └── style.css ├── classes │ ├── AbstractNavigator.html │ ├── Auth0ClientFactory.html │ ├── AuthClientConfig.html │ ├── AuthGuard.html │ ├── AuthHttpInterceptor.html │ ├── AuthModule.html │ ├── AuthService.html │ ├── AuthState.html │ ├── AuthenticationError.html │ ├── GenericError.html │ ├── InMemoryCache.html │ ├── LocalStorageCache.html │ ├── MfaRequiredError.html │ ├── MissingRefreshTokenError.html │ ├── PopupCancelledError.html │ ├── PopupTimeoutError.html │ ├── TimeoutError.html │ └── User.html ├── enums │ └── HttpMethod.html ├── functions │ ├── authGuardFn.html │ ├── authHttpInterceptorFn.html │ ├── isHttpInterceptorRouteConfig.html │ └── provideAuth0.html ├── index.html ├── interfaces │ ├── AppState.html │ ├── AuthConfig.html │ ├── AuthorizationParams.html │ ├── GetTokenSilentlyOptions.html │ ├── GetTokenWithPopupOptions.html │ ├── HttpInterceptorConfig.html │ ├── HttpInterceptorRouteConfig.html │ ├── ICache.html │ ├── IdToken.html │ ├── LogoutOptions.html │ ├── PopupConfigOptions.html │ ├── PopupLoginOptions.html │ └── RedirectLoginOptions.html ├── modules.html ├── types │ ├── ApiRouteDefinition.html │ └── Cacheable.html └── variables │ ├── Auth0ClientService.html │ └── AuthConfigService.html ├── jest.config.ts ├── opslevel.yml ├── package-lock.json ├── package.json ├── projects ├── auth0-angular │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── schematics │ │ ├── collection.json │ │ └── ng-add │ │ │ └── index.ts │ ├── scripts │ │ └── update-useragent.js │ ├── src │ │ ├── lib │ │ │ ├── abstract-navigator.spec.ts │ │ │ ├── abstract-navigator.ts │ │ │ ├── auth.client.spec.ts │ │ │ ├── auth.client.ts │ │ │ ├── auth.config.spec.ts │ │ │ ├── auth.config.ts │ │ │ ├── auth.guard.spec.ts │ │ │ ├── auth.guard.ts │ │ │ ├── auth.interceptor.spec.ts │ │ │ ├── auth.interceptor.ts │ │ │ ├── auth.module.ts │ │ │ ├── auth.service.spec.ts │ │ │ ├── auth.service.ts │ │ │ ├── auth.state.ts │ │ │ ├── functional.ts │ │ │ ├── interfaces.ts │ │ │ └── provide.ts │ │ ├── public-api.ts │ │ ├── test-setup.ts │ │ └── useragent.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.schematics.json │ └── tsconfig.spec.json └── playground │ ├── .browserslistrc │ ├── .eslintrc.json │ ├── e2e │ ├── integration │ │ └── playground.cy.ts │ └── tsconfig.json │ ├── jest.config.ts │ ├── karma.conf.js │ ├── src │ ├── app │ │ ├── app-routing.module.ts │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── components │ │ │ ├── child-route.component.ts │ │ │ ├── error.component.ts │ │ │ ├── lazy-module.component.ts │ │ │ ├── nested-child-route.component.ts │ │ │ ├── protected.component.ts │ │ │ └── unprotected.component.ts │ │ ├── lazy-module-routing.module.ts │ │ └── lazy-module.module.ts │ ├── assets │ │ └── .gitkeep │ ├── config │ │ ├── config.json │ │ └── local │ │ │ └── config.json │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── proxy.conf.json │ ├── styles.css │ └── test-setup.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── scripts ├── exec.js └── oidc-provider.js ├── tsconfig.json └── typedoc.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["projects/**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": ["tsconfig.json", "e2e/tsconfig.json"], 9 | "createDefaultProgram": true 10 | }, 11 | "extends": [ 12 | "plugin:@angular-eslint/ng-cli-compat", 13 | "plugin:@angular-eslint/ng-cli-compat--formatting-add-on", 14 | "plugin:@angular-eslint/template/process-inline-templates" 15 | ], 16 | "rules": { 17 | "@typescript-eslint/member-ordering": 0, 18 | "@typescript-eslint/naming-convention": 0, 19 | "prefer-arrow/prefer-arrow-functions": 0 20 | } 21 | }, 22 | { 23 | "files": ["*.html"], 24 | "extends": ["plugin:@angular-eslint/template/recommended"], 25 | "rules": {} 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @auth0/project-dx-sdks-engineer-codeowner 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug Report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Report a bug 2 | description: Have you found a bug or issue? Create a bug report for this library 3 | labels: ['bug'] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Please do not report security vulnerabilities here**. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. 10 | 11 | - type: checkboxes 12 | id: checklist 13 | attributes: 14 | label: Checklist 15 | options: 16 | - label: The issue can be reproduced in the [auth0-angular sample app](https://github.com/auth0-samples/auth0-angular-samples/tree/master/Sample-01) (or N/A). 17 | required: true 18 | - label: I have looked into the [Readme](https://github.com/auth0/auth0-angular#readme), [Examples](https://github.com/auth0/auth0-angular/blob/main/EXAMPLES.md), and [FAQ](https://github.com/auth0/auth0-angular/blob/main/FAQ.md) and have not found a suitable solution or answer. 19 | required: true 20 | - label: I have looked into the [API documentation](https://auth0.github.io/auth0-angular/) and have not found a suitable solution or answer. 21 | required: true 22 | - label: I have searched the [issues](https://github.com/auth0/auth0-angular/issues) and have not found a suitable solution or answer. 23 | required: true 24 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 25 | required: true 26 | - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). 27 | required: true 28 | 29 | - type: textarea 30 | id: description 31 | attributes: 32 | label: Description 33 | description: Provide a clear and concise description of the issue, including what you expected to happen. 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | id: reproduction 39 | attributes: 40 | label: Reproduction 41 | description: Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. 42 | placeholder: | 43 | 1. Step 1... 44 | 2. Step 2... 45 | 3. ... 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: additional-context 51 | attributes: 52 | label: Additional context 53 | description: Other libraries that might be involved, or any other relevant information you think would be useful. 54 | validations: 55 | required: false 56 | 57 | - type: input 58 | id: environment-version 59 | attributes: 60 | label: auth0-angular version 61 | validations: 62 | required: true 63 | 64 | - type: input 65 | id: environment-angular-version 66 | attributes: 67 | label: Angular version 68 | validations: 69 | required: true 70 | 71 | - type: dropdown 72 | id: environment-browser 73 | attributes: 74 | label: Which browsers have you tested in? 75 | multiple: true 76 | options: 77 | - Chrome 78 | - Edge 79 | - Safari 80 | - Firefox 81 | - Opera 82 | - Other 83 | validations: 84 | required: true 85 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature Request.yml: -------------------------------------------------------------------------------- 1 | name: 🧩 Feature request 2 | description: Suggest an idea or a feature for this library 3 | labels: ['feature request'] 4 | 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | options: 11 | - label: I have looked into the [Readme](https://github.com/auth0/auth0-angular#readme), [Examples](https://github.com/auth0/auth0-angular/blob/main/EXAMPLES.md), and [FAQ](https://github.com/auth0/auth0-angular/blob/main/FAQ.md) and have not found a suitable solution or answer. 12 | required: true 13 | - label: I have looked into the [API documentation](https://auth0.github.io/auth0-angular/) and have not found a suitable solution or answer. 14 | required: true 15 | - label: I have searched the [issues](https://github.com/auth0/auth0-angular/issues) and have not found a suitable solution or answer. 16 | required: true 17 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 18 | required: true 19 | - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). 20 | required: true 21 | 22 | - type: textarea 23 | id: description 24 | attributes: 25 | label: Describe the problem you'd like to have solved 26 | description: A clear and concise description of what the problem is. 27 | placeholder: I'm always frustrated when... 28 | validations: 29 | required: true 30 | 31 | - type: textarea 32 | id: ideal-solution 33 | attributes: 34 | label: Describe the ideal solution 35 | description: A clear and concise description of what you want to happen. 36 | validations: 37 | required: true 38 | 39 | - type: textarea 40 | id: alternatives-and-workarounds 41 | attributes: 42 | label: Alternatives and current workarounds 43 | description: A clear and concise description of any alternatives you've considered or any workarounds that are currently in place. 44 | validations: 45 | required: false 46 | 47 | - type: textarea 48 | id: additional-context 49 | attributes: 50 | label: Additional context 51 | description: Add any other context or screenshots about the feature request here. 52 | validations: 53 | required: false 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Auth0 Community 4 | url: https://community.auth0.com 5 | about: Discuss this SDK in the Auth0 Community forums 6 | - name: FAQ 7 | url: https://github.com/auth0/auth0-angular/blob/main/FAQ.md 8 | about: Read the FAQ to get answers to common issues 9 | - name: SDK API Documentation 10 | url: https://auth0.github.io/auth0-angular/ 11 | about: Read the API documentation for this SDK 12 | - name: Library Documentation 13 | url: https://auth0.com/docs/libraries/auth0-angular-spa 14 | about: Read the library docs on Auth0.com 15 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Build package 2 | description: Build the SDK package 3 | 4 | inputs: 5 | node: 6 | description: The Node version to use 7 | required: false 8 | default: 18 9 | 10 | runs: 11 | using: composite 12 | 13 | steps: 14 | - name: Setup Node 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: ${{ inputs.node }} 18 | cache: 'npm' 19 | 20 | - name: Install dependencies 21 | shell: bash 22 | run: npm ci 23 | 24 | - name: Build package 25 | shell: bash 26 | run: npm run build 27 | -------------------------------------------------------------------------------- /.github/actions/get-prerelease/action.yml: -------------------------------------------------------------------------------- 1 | name: Return a boolean indicating if the version contains prerelease identifiers 2 | 3 | # 4 | # Returns a simple true/false boolean indicating whether the version indicates it's a prerelease or not. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | 9 | inputs: 10 | version: 11 | required: true 12 | 13 | outputs: 14 | prerelease: 15 | value: ${{ steps.get_prerelease.outputs.PRERELEASE }} 16 | 17 | runs: 18 | using: composite 19 | 20 | steps: 21 | - id: get_prerelease 22 | shell: bash 23 | run: | 24 | if [[ "${VERSION}" == *"beta"* || "${VERSION}" == *"alpha"* ]]; then 25 | echo "PRERELEASE=true" >> $GITHUB_OUTPUT 26 | else 27 | echo "PRERELEASE=false" >> $GITHUB_OUTPUT 28 | fi 29 | env: 30 | VERSION: ${{ inputs.version }} 31 | -------------------------------------------------------------------------------- /.github/actions/get-release-notes/action.yml: -------------------------------------------------------------------------------- 1 | name: Return the release notes extracted from the body of the PR associated with the release. 2 | 3 | # 4 | # Returns the release notes from the content of a pull request linked to a release branch. It expects the branch name to be in the format release/vX.Y.Z, release/X.Y.Z, release/vX.Y.Z-beta.N. etc. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | inputs: 9 | version: 10 | required: true 11 | repo_name: 12 | required: false 13 | repo_owner: 14 | required: true 15 | token: 16 | required: true 17 | 18 | outputs: 19 | release-notes: 20 | value: ${{ steps.get_release_notes.outputs.RELEASE_NOTES }} 21 | 22 | runs: 23 | using: composite 24 | 25 | steps: 26 | - uses: actions/github-script@v7 27 | id: get_release_notes 28 | with: 29 | result-encoding: string 30 | script: | 31 | const { data: pulls } = await github.rest.pulls.list({ 32 | owner: process.env.REPO_OWNER, 33 | repo: process.env.REPO_NAME, 34 | state: 'all', 35 | head: `${process.env.REPO_OWNER}:release/${process.env.VERSION}`, 36 | }); 37 | core.setOutput('RELEASE_NOTES', pulls[0].body); 38 | env: 39 | GITHUB_TOKEN: ${{ inputs.token }} 40 | REPO_OWNER: ${{ inputs.repo_owner }} 41 | REPO_NAME: ${{ inputs.repo_name }} 42 | VERSION: ${{ inputs.version }} 43 | -------------------------------------------------------------------------------- /.github/actions/get-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Return the version extracted from the branch name 2 | 3 | # 4 | # Returns the version from the .version file. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | 9 | outputs: 10 | version: 11 | value: ${{ steps.get_version.outputs.VERSION }} 12 | 13 | runs: 14 | using: composite 15 | 16 | steps: 17 | - id: get_version 18 | shell: bash 19 | run: | 20 | VERSION=$(head -1 .version) 21 | echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT 22 | -------------------------------------------------------------------------------- /.github/actions/npm-publish/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish release to npm 2 | 3 | inputs: 4 | node-version: 5 | required: true 6 | npm-token: 7 | required: true 8 | version: 9 | required: true 10 | require-build: 11 | default: true 12 | release-directory: 13 | default: './' 14 | 15 | runs: 16 | using: composite 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ inputs.node-version }} 26 | cache: 'npm' 27 | registry-url: 'https://registry.npmjs.org' 28 | 29 | - name: Install dependencies 30 | shell: bash 31 | run: npm ci --include=dev 32 | 33 | - name: Build package 34 | if: inputs.require-build == 'true' 35 | shell: bash 36 | run: npm run build 37 | 38 | - name: Publish release to NPM 39 | shell: bash 40 | working-directory: ${{ inputs.release-directory }} 41 | run: | 42 | if [[ "${VERSION}" == *"beta"* ]]; then 43 | TAG="beta" 44 | elif [[ "${VERSION}" == *"alpha"* ]]; then 45 | TAG="alpha" 46 | else 47 | TAG="latest" 48 | fi 49 | npm publish --provenance --tag $TAG 50 | env: 51 | NODE_AUTH_TOKEN: ${{ inputs.npm-token }} 52 | VERSION: ${{ inputs.version }} 53 | -------------------------------------------------------------------------------- /.github/actions/release-create/action.yml: -------------------------------------------------------------------------------- 1 | name: Create a GitHub release 2 | 3 | # 4 | # Creates a GitHub release with the given version. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | 9 | inputs: 10 | token: 11 | required: true 12 | files: 13 | required: false 14 | name: 15 | required: true 16 | body: 17 | required: true 18 | tag: 19 | required: true 20 | commit: 21 | required: true 22 | draft: 23 | default: false 24 | required: false 25 | prerelease: 26 | default: false 27 | required: false 28 | fail_on_unmatched_files: 29 | default: true 30 | required: false 31 | 32 | runs: 33 | using: composite 34 | 35 | steps: 36 | - uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 37 | with: 38 | body: ${{ inputs.body }} 39 | name: ${{ inputs.name }} 40 | tag_name: ${{ inputs.tag }} 41 | target_commitish: ${{ inputs.commit }} 42 | draft: ${{ inputs.draft }} 43 | prerelease: ${{ inputs.prerelease }} 44 | fail_on_unmatched_files: ${{ inputs.fail_on_unmatched_files }} 45 | files: ${{ inputs.files }} 46 | env: 47 | GITHUB_TOKEN: ${{ inputs.token }} 48 | -------------------------------------------------------------------------------- /.github/actions/rl-scanner/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Reversing Labs Scanner' 2 | description: 'Runs the Reversing Labs scanner on a specified artifact.' 3 | inputs: 4 | artifact-path: 5 | description: 'Path to the artifact to be scanned.' 6 | required: true 7 | version: 8 | description: 'Version of the artifact.' 9 | required: true 10 | 11 | runs: 12 | using: 'composite' 13 | steps: 14 | - name: Set up Python 15 | uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.10' 18 | 19 | - name: Install Python dependencies 20 | shell: bash 21 | run: | 22 | pip install boto3 requests 23 | 24 | - name: Configure AWS credentials 25 | uses: aws-actions/configure-aws-credentials@v1 26 | with: 27 | role-to-assume: ${{ env.PRODSEC_TOOLS_ARN }} 28 | aws-region: us-east-1 29 | mask-aws-account-id: true 30 | 31 | - name: Install RL Wrapper 32 | shell: bash 33 | run: | 34 | pip install rl-wrapper>=1.0.0 --index-url "https://${{ env.PRODSEC_TOOLS_USER }}:${{ env.PRODSEC_TOOLS_TOKEN }}@a0us.jfrog.io/artifactory/api/pypi/python-local/simple" 35 | 36 | - name: Run RL Scanner 37 | shell: bash 38 | env: 39 | RLSECURE_LICENSE: ${{ env.RLSECURE_LICENSE }} 40 | RLSECURE_SITE_KEY: ${{ env.RLSECURE_SITE_KEY }} 41 | SIGNAL_HANDLER_TOKEN: ${{ env.SIGNAL_HANDLER_TOKEN }} 42 | PYTHONUNBUFFERED: 1 43 | run: | 44 | if [ ! -f "${{ inputs.artifact-path }}" ]; then 45 | echo "Artifact not found: ${{ inputs.artifact-path }}" 46 | exit 1 47 | fi 48 | 49 | rl-wrapper \ 50 | --artifact "${{ inputs.artifact-path }}" \ 51 | --name "${{ github.event.repository.name }}" \ 52 | --version "${{ inputs.version }}" \ 53 | --repository "${{ github.repository }}" \ 54 | --commit "${{ github.sha }}" \ 55 | --build-env "github_actions" \ 56 | --suppress_output 57 | 58 | # Check the outcome of the scanner 59 | if [ $? -ne 0 ]; then 60 | echo "RL Scanner failed." 61 | echo "scan-status=failed" >> $GITHUB_ENV 62 | exit 1 63 | else 64 | echo "RL Scanner passed." 65 | echo "scan-status=success" >> $GITHUB_ENV 66 | fi 67 | 68 | outputs: 69 | scan-status: 70 | description: 'The outcome of the scan process.' 71 | value: ${{ env.scan-status }} 72 | -------------------------------------------------------------------------------- /.github/actions/tag-exists/action.yml: -------------------------------------------------------------------------------- 1 | name: Return a boolean indicating if a tag already exists for the repository 2 | 3 | # 4 | # Returns a simple true/false boolean indicating whether the tag exists or not. 5 | # 6 | # TODO: Remove once the common repo is public. 7 | # 8 | 9 | inputs: 10 | token: 11 | required: true 12 | tag: 13 | required: true 14 | 15 | outputs: 16 | exists: 17 | description: 'Whether the tag exists or not' 18 | value: ${{ steps.tag-exists.outputs.EXISTS }} 19 | 20 | runs: 21 | using: composite 22 | 23 | steps: 24 | - id: tag-exists 25 | shell: bash 26 | run: | 27 | GET_API_URL="https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG_NAME}" 28 | http_status_code=$(curl -LI $GET_API_URL -o /dev/null -w '%{http_code}\n' -s -H "Authorization: token ${GITHUB_TOKEN}") 29 | if [ "$http_status_code" -ne "404" ] ; then 30 | echo "EXISTS=true" >> $GITHUB_OUTPUT 31 | else 32 | echo "EXISTS=false" >> $GITHUB_OUTPUT 33 | fi 34 | env: 35 | TAG_NAME: ${{ inputs.tag }} 36 | GITHUB_TOKEN: ${{ inputs.token }} 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | groups: 8 | angular: 9 | patterns: 10 | - "@angular*" 11 | update-types: 12 | - "minor" 13 | ignore: 14 | - dependency-name: "*" 15 | update-types: ["version-update:semver-major"] 16 | - package-ecosystem: 'github-actions' 17 | directory: '/' 18 | schedule: 19 | interval: 'daily' 20 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 30 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | daysUntilClose: 7 8 | 9 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 10 | onlyLabels: 11 | - 'waiting for customer' 12 | 13 | # Ignore issues in projects 14 | exemptProjects: true 15 | 16 | # Ignore issues and PRs in milestones 17 | exemptMilestones: true 18 | 19 | # Set to true to ignore issues with an assignee 20 | exemptAssignees: true 21 | 22 | # Label to use when marking as stale 23 | staleLabel: closed:stale 24 | 25 | # Comment to post when marking as stale. Set to `false` to disable 26 | markComment: > 27 | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇‍♂️ 28 | -------------------------------------------------------------------------------- /.github/workflows/browserstack.yml: -------------------------------------------------------------------------------- 1 | name: Browserstack 2 | 3 | on: 4 | merge_group: 5 | workflow_dispatch: 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | push: 11 | branches: 12 | - main 13 | 14 | permissions: 15 | contents: read 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 19 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 20 | 21 | env: 22 | NODE_VERSION: 18 23 | CACHE_KEY: '${{ github.event.pull_request.head.sha || github.ref }}-${{ github.run_id }}-${{ github.run_attempt }}' 24 | 25 | jobs: 26 | 27 | browserstack: 28 | 29 | name: BrowserStack Tests 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - name: Checkout code 34 | uses: actions/checkout@v4 35 | with: 36 | ref: ${{ github.event.pull_request.head.sha || github.ref }} 37 | 38 | - name: Build package 39 | uses: ./.github/actions/build 40 | with: 41 | node: ${{ env.NODE_VERSION }} 42 | 43 | - name: Run tests 44 | shell: bash 45 | run: npx concurrently --kill-others --success first 'npm run start:local:oidc' 'npx wait-on tcp:127.0.0.1:3000 && npm run start:local:playground' 'npx wait-on tcp:127.0.0.1:4200 && npx browserstack-cypress-cli run --build-name ${{ github.ref }}-${{ github.sha }}' 46 | env: 47 | BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 48 | BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }} 49 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | types: 7 | - opened 8 | - synchronize 9 | push: 10 | branches: 11 | - main 12 | schedule: 13 | - cron: '37 10 * * 2' 14 | 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | concurrency: 21 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 22 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 23 | 24 | jobs: 25 | analyze: 26 | name: Check for Vulnerabilities 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [javascript] 33 | 34 | steps: 35 | - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' 36 | run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection. 37 | 38 | - name: Checkout 39 | uses: actions/checkout@v4 40 | 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v3 43 | with: 44 | languages: ${{ matrix.language }} 45 | queries: +security-and-quality 46 | 47 | - name: Autobuild 48 | uses: github/codeql-action/autobuild@v3 49 | 50 | - name: Perform CodeQL Analysis 51 | uses: github/codeql-action/analyze@v3 52 | with: 53 | category: '/language:${{ matrix.language }}' 54 | -------------------------------------------------------------------------------- /.github/workflows/npm-release.yml: -------------------------------------------------------------------------------- 1 | name: Create npm and GitHub Release 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | node-version: 7 | required: true 8 | type: string 9 | require-build: 10 | default: true 11 | type: string 12 | release-directory: 13 | default: './' 14 | type: string 15 | secrets: 16 | github-token: 17 | required: true 18 | npm-token: 19 | required: true 20 | 21 | jobs: 22 | release: 23 | if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) 24 | runs-on: ubuntu-latest 25 | environment: release 26 | 27 | steps: 28 | # Checkout the code 29 | - uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | # Get the version from the branch name 34 | - id: get_version 35 | uses: ./.github/actions/get-version 36 | 37 | # Get the prerelease flag from the branch name 38 | - id: get_prerelease 39 | uses: ./.github/actions/get-prerelease 40 | with: 41 | version: ${{ steps.get_version.outputs.version }} 42 | 43 | # Get the release notes 44 | - id: get_release_notes 45 | uses: ./.github/actions/get-release-notes 46 | with: 47 | token: ${{ secrets.github-token }} 48 | version: ${{ steps.get_version.outputs.version }} 49 | repo_owner: ${{ github.repository_owner }} 50 | repo_name: ${{ github.event.repository.name }} 51 | 52 | # Check if the tag already exists 53 | - id: tag_exists 54 | uses: ./.github/actions/tag-exists 55 | with: 56 | tag: ${{ steps.get_version.outputs.version }} 57 | token: ${{ secrets.github-token }} 58 | 59 | # If the tag already exists, exit with an error 60 | - if: steps.tag_exists.outputs.exists == 'true' 61 | run: exit 1 62 | 63 | # Publish the release to our package manager 64 | - uses: ./.github/actions/npm-publish 65 | with: 66 | node-version: ${{ inputs.node-version }} 67 | require-build: ${{ inputs.require-build }} 68 | version: ${{ steps.get_version.outputs.version }} 69 | npm-token: ${{ secrets.npm-token }} 70 | release-directory: ${{ inputs.release-directory }} 71 | 72 | # Create a release for the tag 73 | - uses: ./.github/actions/release-create 74 | with: 75 | token: ${{ secrets.github-token }} 76 | name: ${{ steps.get_version.outputs.version }} 77 | body: ${{ steps.get_release_notes.outputs.release-notes }} 78 | tag: ${{ steps.get_version.outputs.version }} 79 | commit: ${{ github.sha }} 80 | prerelease: ${{ steps.get_prerelease.outputs.prerelease }} 81 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | branch: 7 | description: The branch to release from 8 | required: true 9 | default: main 10 | version: 11 | description: The version being published. This should be a valid semver version, such as `1.0.0`. 12 | required: true 13 | default: '' 14 | type: string 15 | dry-run: 16 | type: boolean 17 | description: Perform a publishing dry run. This will not publish the release, but will validate the release and log the commands that would be run. 18 | default: false 19 | 20 | permissions: 21 | contents: read 22 | id-token: write # For publishing to NPM with provenance. Allows developers to run `npm audit signatures` and verify release signature of SDK. @see https://github.blog/2023-04-19-introducing-npm-package-provenance/ 23 | 24 | env: 25 | NODE_VERSION: 18 26 | NODE_ENV: development 27 | 28 | jobs: 29 | configure: 30 | name: Validate input parameters 31 | runs-on: ubuntu-latest 32 | 33 | outputs: 34 | vtag: ${{ steps.vtag.outputs.vtag }} # The fully constructed release tag to use for publishing 35 | dry-run: ${{ steps.dry-run.outputs.dry-run }} # The dry-run flag to use for publishing, if applicable 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 0 42 | ref: ${{ github.event.inputs.branch }} 43 | 44 | # Configure for dry-run, if applicable. @see https://docs.npmjs.com/cli/v9/commands/npm-publish#dry-run 45 | - id: dry-run 46 | if: ${{ github.event.inputs.dry-run == 'true' }} 47 | name: Configure for `--dry-run` 48 | run: | 49 | echo "dry-run=--dry-run" >> $GITHUB_ENV 50 | echo "dry-run=--dry-run" >> $GITHUB_OUTPUT 51 | 52 | # Build the tag string from package.json version and release suffix. Produces something like `1.0.0-beta.1` for a beta, or `1.0.0` for a stable release. 53 | - name: Build tag 54 | id: vtag 55 | env: 56 | PACKAGE_VERSION="${{ github.event.inputs.version }}" 57 | run: | 58 | echo "vtag=${PACKAGE_VERSION}" >> $GITHUB_ENV 59 | echo "vtag=${PACKAGE_VERSION}" >> $GITHUB_OUTPUT 60 | 61 | # Ensure tag does not already exist. 62 | - name: Validate version 63 | uses: actions/github-script@v7 64 | env: 65 | vtag: ${{ env.vtag }} 66 | with: 67 | script: | 68 | const releaseMeta = github.rest.repos.listReleases.endpoint.merge({ 69 | owner: context.repo.owner, 70 | repo: context.repo.repo, 71 | }); 72 | 73 | const releases = await github.paginate(releaseMeta); 74 | 75 | for (const release of releases) { 76 | if (release.name === process.env.vtag) { 77 | throw new Error(`${process.env.vtag} already exists`); 78 | } 79 | } 80 | 81 | console.log(`${process.env.vtag} does not exist. Proceeding with release.`) 82 | 83 | publish-npm: 84 | needs: configure 85 | 86 | name: Publish to NPM 87 | runs-on: ubuntu-latest 88 | environment: release 89 | 90 | steps: 91 | - name: Checkout code 92 | uses: actions/checkout@v4 93 | with: 94 | fetch-depth: 0 95 | ref: ${{ github.event.inputs.branch }} 96 | 97 | - name: Setup Node 98 | uses: actions/setup-node@v4 99 | with: 100 | node-version: ${{ env.NODE_VERSION }} 101 | cache: npm 102 | registry-url: 'https://registry.npmjs.org' 103 | 104 | - name: Install dependencies 105 | run: npm ci 106 | 107 | - name: Publish release to NPM 108 | run: npm publish --provenance --tag ${{ needs.configure.outputs.vtag }} ${{ needs.configure.outputs.dry-run }} 109 | env: 110 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 111 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create npm and GitHub Release 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: write 11 | id-token: write # For publishing to npm using --provenance 12 | 13 | ### TODO: Replace instances of './.github/workflows/' w/ `auth0/dx-sdk-actions/workflows/` and append `@latest` after the common `dx-sdk-actions` repo is made public. 14 | ### TODO: Also remove `get-prerelease`, `get-release-notes`, `get-version`, `npm-publish`, `release-create`, and `tag-exists` actions from this repo's .github/actions folder once the repo is public. 15 | ### TODO: Also remove `npm-release` workflow from this repo's .github/workflows folder once the repo is public. 16 | 17 | jobs: 18 | rl-scanner: 19 | uses: ./.github/workflows/rl-secure.yml 20 | with: 21 | node-version: 18 ## depends if build requires node else we can remove this. 22 | artifact-name: 'auth0-angular.tgz' ## Will change respective to Repository 23 | secrets: 24 | RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} 25 | RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} 26 | SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} 27 | PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} 28 | PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} 29 | PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} 30 | 31 | release: 32 | uses: ./.github/workflows/npm-release.yml 33 | needs: rl-scanner ## this is important as this will not let release job to run until rl-scanner is done 34 | with: 35 | node-version: 18 36 | require-build: true 37 | release-directory: './dist/auth0-angular' 38 | secrets: 39 | npm-token: ${{ secrets.NPM_TOKEN }} 40 | github-token: ${{ secrets.GITHUB_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/rl-secure.yml: -------------------------------------------------------------------------------- 1 | name: RL-Secure Workflow 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | node-version: ## depends if build requires node else we can remove this. 7 | required: true 8 | type: string 9 | artifact-name: 10 | required: true 11 | type: string 12 | secrets: 13 | RLSECURE_LICENSE: 14 | required: true 15 | RLSECURE_SITE_KEY: 16 | required: true 17 | SIGNAL_HANDLER_TOKEN: 18 | required: true 19 | PRODSEC_TOOLS_USER: 20 | required: true 21 | PRODSEC_TOOLS_TOKEN: 22 | required: true 23 | PRODSEC_TOOLS_ARN: 24 | required: true 25 | 26 | jobs: 27 | rl-scanner: 28 | name: Run Reversing Labs Scanner 29 | if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged && startsWith(github.event.pull_request.head.ref, 'release/')) 30 | runs-on: ubuntu-latest 31 | outputs: 32 | scan-status: ${{ steps.rl-scan-conclusion.outcome }} 33 | 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 39 | 40 | - name: Build package 41 | uses: ./.github/actions/build 42 | with: 43 | node: ${{ inputs.node-version }} 44 | 45 | - name: Create tgz build artifact 46 | run: | 47 | tar -czvf ${{ inputs.artifact-name }} * 48 | 49 | - id: get_version 50 | uses: ./.github/actions/get-version 51 | 52 | - name: Run RL Scanner 53 | id: rl-scan-conclusion 54 | uses: ./.github/actions/rl-scanner 55 | with: 56 | artifact-path: "$(pwd)/${{ inputs.artifact-name }}" 57 | version: "${{ steps.get_version.outputs.version }}" 58 | env: 59 | RLSECURE_LICENSE: ${{ secrets.RLSECURE_LICENSE }} 60 | RLSECURE_SITE_KEY: ${{ secrets.RLSECURE_SITE_KEY }} 61 | SIGNAL_HANDLER_TOKEN: ${{ secrets.SIGNAL_HANDLER_TOKEN }} 62 | PRODSEC_TOOLS_USER: ${{ secrets.PRODSEC_TOOLS_USER }} 63 | PRODSEC_TOOLS_TOKEN: ${{ secrets.PRODSEC_TOOLS_TOKEN }} 64 | PRODSEC_TOOLS_ARN: ${{ secrets.PRODSEC_TOOLS_ARN }} 65 | 66 | - name: Output scan result 67 | run: echo "scan-status=${{ steps.rl-scan-conclusion.outcome }}" >> $GITHUB_ENV -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | types: 7 | - opened 8 | - synchronize 9 | push: 10 | branches: 11 | - main 12 | schedule: 13 | - cron: '30 0 1,15 * *' 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 20 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 21 | 22 | jobs: 23 | 24 | run: 25 | 26 | name: Check for Vulnerabilities 27 | runs-on: ubuntu-latest 28 | 29 | container: 30 | image: returntocorp/semgrep 31 | 32 | steps: 33 | - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' 34 | run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection. 35 | 36 | - uses: actions/checkout@v4 37 | with: 38 | ref: ${{ github.event.pull_request.head.sha || github.ref }} 39 | 40 | - run: semgrep ci 41 | env: 42 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/snyk.yml: -------------------------------------------------------------------------------- 1 | name: Snyk 2 | 3 | on: 4 | merge_group: 5 | workflow_dispatch: 6 | pull_request: 7 | types: 8 | - opened 9 | - synchronize 10 | push: 11 | branches: 12 | - main 13 | schedule: 14 | - cron: '30 0 1,15 * *' 15 | 16 | permissions: 17 | contents: read 18 | 19 | concurrency: 20 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 21 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 22 | 23 | jobs: 24 | 25 | check: 26 | 27 | name: Check for Vulnerabilities 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' 32 | run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection. 33 | 34 | - uses: actions/checkout@v4 35 | with: 36 | ref: ${{ github.event.pull_request.head.sha || github.ref }} 37 | 38 | - uses: snyk/actions/node@b98d498629f1c368650224d6d212bf7dfa89e4bf # pin@0.4.0 39 | env: 40 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | merge_group: 5 | workflow_dispatch: 6 | pull_request: 7 | branches: 8 | - main 9 | push: 10 | branches: 11 | - main 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 19 | 20 | env: 21 | NODE_VERSION: 18 22 | CACHE_KEY: '${{ github.ref }}-${{ github.run_id }}-${{ github.run_attempt }}' 23 | 24 | jobs: 25 | build: 26 | name: Build Package 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v4 32 | 33 | - name: Build package 34 | uses: ./.github/actions/build 35 | with: 36 | node: ${{ env.NODE_VERSION }} 37 | 38 | - name: Save build artifacts 39 | uses: actions/cache/save@v4 40 | with: 41 | path: . 42 | key: ${{ env.CACHE_KEY }} 43 | 44 | unit: 45 | needs: build # Require build to complete before running tests 46 | 47 | name: Unit Tests 48 | runs-on: ubuntu-latest 49 | 50 | steps: 51 | - name: Checkout code 52 | uses: actions/checkout@v4 53 | 54 | - name: Setup Node 55 | uses: actions/setup-node@v4 56 | with: 57 | node-version: ${{ env.NODE_VERSION }} 58 | cache: npm 59 | 60 | - name: Restore build artifacts 61 | uses: actions/cache/restore@v4 62 | with: 63 | path: . 64 | key: ${{ env.CACHE_KEY }} 65 | 66 | - name: Run tests 67 | run: npm run test:ci 68 | 69 | - name: Upload coverage 70 | uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # pin@3.1.4 71 | 72 | lint: 73 | needs: build # Require build to complete before running tests 74 | 75 | name: Lint Tests 76 | runs-on: ubuntu-latest 77 | 78 | steps: 79 | - name: Checkout code 80 | uses: actions/checkout@v4 81 | 82 | - name: Setup Node 83 | uses: actions/setup-node@v4 84 | with: 85 | node-version: ${{ env.NODE_VERSION }} 86 | cache: npm 87 | 88 | - name: Restore build artifacts 89 | uses: actions/cache/restore@v4 90 | with: 91 | path: . 92 | key: ${{ env.CACHE_KEY }} 93 | 94 | - name: Run tests 95 | run: npm run lint 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # cypress 4 | projects/playground/e2e/videos 5 | projects/playground/e2e/screenshots 6 | cypress.env.json 7 | 8 | # release pipeline tmp files 9 | .release*/ 10 | .release 11 | 12 | # compiled output 13 | /dist 14 | /tmp 15 | /out-tsc 16 | # Only exists if Bazel was run 17 | /bazel-out 18 | 19 | # dependencies 20 | /node_modules 21 | 22 | # profiling files 23 | chrome-profiler-events*.json 24 | speed-measure-plugin*.json 25 | 26 | # IDEs and editors 27 | /.idea 28 | .project 29 | .classpath 30 | .c9/ 31 | *.launch 32 | .settings/ 33 | *.sublime-workspace 34 | 35 | # IDE - VSCode 36 | .vscode/* 37 | !.vscode/settings.json 38 | !.vscode/tasks.json 39 | !.vscode/launch.json 40 | !.vscode/extensions.json 41 | .history/* 42 | 43 | # misc 44 | /.angular/cache 45 | /.sass-cache 46 | /connect.lock 47 | /coverage 48 | /libpeerconnection.log 49 | npm-debug.log 50 | yarn-error.log 51 | testem.log 52 | /typings 53 | 54 | # System Files 55 | .DS_Store 56 | Thumbs.db 57 | 58 | # Tests 59 | test-results -------------------------------------------------------------------------------- /.semgrepignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | projects/playground/ 3 | api-server.js 4 | -------------------------------------------------------------------------------- /.shiprc: -------------------------------------------------------------------------------- 1 | { 2 | "packagePath": "projects/auth0-angular", 3 | "files": { 4 | ".version": [] 5 | }, 6 | "postbump": "npm run docs" 7 | } -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | v2.2.3 -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-vscode.vscode-typescript-tslint-plugin", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Examples using auth0-angular 2 | 3 | - [Add login to your application](#add-login-to-your-application) 4 | - [Add logout to your application](#add-logout-to-your-application) 5 | - [Checking if a user is authenticated](#checking-if-a-user-is-authenticated) 6 | - [Display the user profile](#display-the-user-profile) 7 | - [Protect a route](#protect-a-route) 8 | - [Call an API](#call-an-api) 9 | - [Handling errors](#handling-errors) 10 | - [Organizations](#organizations) 11 | - [Standalone Components and a more functional approach](#standalone-components-and-a-more-functional-approach) 12 | 13 | ## Add login to your application 14 | 15 | To log the user into the application, inject the `AuthService` and call its `loginWithRedirect` method. 16 | 17 | ```ts 18 | import { Component } from '@angular/core'; 19 | import { AuthService } from '@auth0/auth0-angular'; 20 | 21 | @Component({ 22 | selector: 'app-root', 23 | templateUrl: './app.component.html', 24 | styleUrls: ['./app.component.css'], 25 | }) 26 | export class AppComponent { 27 | constructor(public auth: AuthService) {} 28 | 29 | loginWithRedirect() { 30 | this.auth.loginWithRedirect(); 31 | } 32 | ``` 33 | 34 | By default the application will ask Auth0 to redirect back to the root URL of your application after authentication. This can be configured by setting the [redirectUri](https://auth0.github.io/auth0-angular/interfaces/auth_config.AuthConfig.html#redirectUri) option. 35 | 36 | ## Add logout to your application 37 | 38 | To log the user out of your application, call the `logout` method on `AuthService` from anywhere inside your application, such as a component: 39 | 40 | ```ts 41 | import { Component } from '@angular/core'; 42 | import { AuthService } from '@auth0/auth0-angular'; 43 | 44 | @Component({ 45 | selector: 'app-root', 46 | templateUrl: './app.component.html', 47 | styleUrls: ['./app.component.css'], 48 | }) 49 | export class AppComponent { 50 | constructor(public auth: AuthService) {} 51 | 52 | logout() { 53 | this.auth.logout(); 54 | } 55 | ``` 56 | 57 | ## Checking if a user is authenticated 58 | 59 | The `isAuthenticated$` observable on `AuthService` emits true or false based on the current authentication state. You can use this observable to make any decisions based on whether or not the user is authenticated, such as only showing the login button when the user is not logged in yet, and the logout button only when the user is logged in. 60 | 61 | ```ts 62 | 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 76 | 77 | ``` 78 | 79 | ## Display the user profile 80 | 81 | Access the `user$` observable on the `AuthService` instance to retrieve the user profile. This observable already heeds the `isAuthenticated$` observable, so you do not need to check if the user is authenticated before using it: 82 | 83 | ```ts 84 | import { Component } from '@angular/core'; 85 | import { AuthService } from '@auth0/auth0-angular'; 86 | 87 | @Component({ 88 | selector: 'app-profile', 89 | templateUrl: './app.component.html', 90 | styleUrls: ['./app.component.css'], 91 | }) 92 | export class ProfileComponent { 93 | user$ = this.auth.user$; 94 | 95 | constructor(public auth: AuthService) {} 96 | ``` 97 | 98 | You can then access the component's `user$` observable from within your template. 99 | 100 | ```html 101 | 105 | ``` 106 | 107 | ## Protect a route 108 | 109 | To ensure that a route can only be visited by authenticated users, add the built-in `AuthGuard` type to the `canActivate` property on the route you wish to protect. 110 | 111 | If an unauthenticated user tries to access this route, they will first be redirected to Auth0 to log in before returning to the URL they tried to get to before login: 112 | 113 | ```ts 114 | import { NgModule } from '@angular/core'; 115 | import { Routes, RouterModule } from '@angular/router'; 116 | import { HomeComponent } from './unprotected/unprotected.component'; 117 | import { ProtectedComponent } from './protected/protected.component'; 118 | import { AuthGuard } from '@auth0/auth0-angular'; 119 | 120 | const routes: Routes = [ 121 | { 122 | path: 'protected', 123 | component: ProtectedComponent, 124 | canActivate: [AuthGuard], 125 | }, 126 | { 127 | path: '', 128 | component: HomeComponent, 129 | pathMatch: 'full', 130 | }, 131 | ]; 132 | 133 | @NgModule({ 134 | imports: [RouterModule.forRoot(routes)], 135 | exports: [RouterModule], 136 | }) 137 | export class AppRoutingModule {} 138 | ``` 139 | 140 | ## Call an API 141 | 142 | The SDK provides an `HttpInterceptor` that automatically attaches access tokens to outgoing requests when using the built-in `HttpClient`. However, you must provide configuration that tells the interceptor which requests to attach access tokens to. 143 | 144 | ### Specify the audience 145 | 146 | In order for Auth0 to be able to issue tokens for a specific API, we need to configure the Audience to inform Auth0 about the API in question. Set the `audience`, when calling `AuthModule.forRoot()`, to the **API Identifier** of the API from within your Auth0 dashboard. 147 | 148 | ```ts 149 | import { NgModule } from '@angular/core'; 150 | import { AuthModule } from '@auth0/auth0-angular'; 151 | 152 | @NgModule({ 153 | // ... 154 | imports: [ 155 | AuthModule.forRoot({ 156 | domain: 'YOUR_AUTH0_DOMAIN', 157 | clientId: 'YOUR_AUTH0_CLIENT_ID', 158 | authorizationParams: { 159 | audience: 'YOUR_AUTH0_API_IDENTIFIER', 160 | } 161 | }), 162 | ], 163 | // ... 164 | }) 165 | export class AppModule {} 166 | ``` 167 | 168 | ### Register AuthHttpInterceptor 169 | 170 | First, register the interceptor with your application module, along with the `HttpClientModule`. 171 | 172 | ```ts 173 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 174 | import { AuthHttpInterceptor } from '@auth0/auth0-angular'; 175 | 176 | @NgModule({ 177 | // ... 178 | imports: [ 179 | HttpClientModule, 180 | AuthModule.forRoot(...), 181 | ], 182 | providers: [ 183 | { provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true }, 184 | ], 185 | // ... 186 | }) 187 | ``` 188 | 189 | Note: We do not do this automatically for you as we want you to be explicit about including this interceptor. Also, you may want to chain this interceptor with others, making it hard for us to place it accurately. 190 | 191 | ### Configure AuthHttpInterceptor to attach access tokens 192 | 193 | Next, tell the SDK which requests to attach access tokens to in the SDK configuration. These are matched on the URL by using a string, a regex, or more complex object that also allows you to specify the configuration for fetching tokens by setting the `tokenOptions` property. 194 | 195 | If an HTTP call is made using `HttpClient` and there is no match in this configuration for that URL, then the interceptor will simply be bypassed and the call will be executed without a token attached in the `Authorization` header. 196 | 197 | Note: We do this to help prevent tokens being unintentionally attached to requests to the wrong recipient, which is a serious security issue. Those recipients could then use that token to call the API as if it were your application. 198 | 199 | In the event that requests should be made available for both anonymous and authenticated users, the `allowAnonymous` property can be set to `true`. When omitted, or set to `false`, requests that match the configuration, will not be executed when there is no access token available. 200 | 201 | Here are some examples: 202 | 203 | ```ts 204 | import { HttpMethod } from '@auth0/auth0-angular'; 205 | 206 | // Modify your existing SDK configuration to include the httpInterceptor config 207 | AuthModule.forRoot({ 208 | ... 209 | // The AuthHttpInterceptor configuration 210 | httpInterceptor: { 211 | allowedList: [ 212 | // Attach access tokens to any calls to '/api' (exact match) 213 | '/api', 214 | 215 | // Attach access tokens to any calls that start with '/api/' 216 | '/api/*', 217 | 218 | // Match anything starting with /api/products, but also allow for anonymous users. 219 | { 220 | uri: '/api/products/*', 221 | allowAnonymous: true, 222 | }, 223 | 224 | // Match anything starting with /api/accounts, but also specify the audience and scope the attached 225 | // access token must have 226 | { 227 | uri: '/api/accounts/*', 228 | tokenOptions: { 229 | authorizationParams: { 230 | audience: 'http://my-api/', 231 | scope: 'read:accounts', 232 | } 233 | }, 234 | }, 235 | 236 | // Matching on HTTP method 237 | { 238 | uri: '/api/orders', 239 | httpMethod: HttpMethod.Post, 240 | tokenOptions: { 241 | authorizationParams: { 242 | audience: 'http://my-api/', 243 | scope: 'write:orders', 244 | } 245 | }, 246 | }, 247 | 248 | // Using an absolute URI 249 | { 250 | uri: 'https://your-domain.auth0.com/api/v2/users', 251 | tokenOptions: { 252 | authorizationParams: { 253 | audience: 'https://your-domain.com/api/v2/', 254 | scope: 'read:users', 255 | } 256 | }, 257 | }, 258 | ], 259 | }, 260 | }); 261 | ``` 262 | 263 | **Note:** Under the hood, tokenOptions is passed as-is to [the getTokenSilently method](https://auth0.github.io/auth0-spa-js/classes/Auth0Client.html#getTokenSilently) on the underlying SDK, so all the same options apply here. 264 | 265 | **Uri matching** 266 | 267 | If you need more fine-grained control over the URI matching, you can provide a callback function to the `uriMatcher` property that takes a single `uri` argument (being [HttpRequest.url](https://angular.io/api/common/http/HttpRequest#url)) and returns a boolean. If this function returns true, then an access token is attached to the request in the ["Authorization" header](https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-20#section-2.1). If it returns false, the request proceeds without the access token attached. 268 | 269 | ```ts 270 | AuthModule.forRoot({ 271 | // ... 272 | httpInterceptor: { 273 | allowedList: [ 274 | { 275 | uriMatcher: (uri) => uri.indexOf('/api/orders') > -1, 276 | httpMethod: HttpMethod.Post, 277 | tokenOptions: { 278 | authorizationParams: { 279 | audience: 'http://my-api/', 280 | scope: 'write:orders', 281 | } 282 | }, 283 | }, 284 | ], 285 | }, 286 | }); 287 | ``` 288 | 289 | You might want to do this in scenarios where you need the token on multiple endpoints, but want to exclude it from only a few other endpoints. Instead of explicitly listing all endpoints that do need a token, a uriMatcher can be used to include all but the few endpoints that do not need a token attached to its requests. 290 | 291 | ## Handling errors 292 | 293 | Whenever the SDK fails to retrieve an Access Token, either as part of the above interceptor or when manually calling `AuthService.getAccessTokenSilently` and `AuthService.getAccessTokenWithPopup`, it will emit the corresponding error in the `AuthService.error$` observable. 294 | 295 | If you want to react to these errors, subscribe to the `error$` observable and act accordingly. 296 | 297 | ```ts 298 | ngOnInit() { 299 | this.authService.error$.subscribe(error => { 300 | // Handle Error here 301 | }); 302 | } 303 | ``` 304 | 305 | A common reason you might want to handle the above errors, emitted by the `error$` observable, is to re-login the user when the SDK throws a `login_required` error. 306 | 307 | ```ts 308 | ngOnInit() { 309 | this.authService.error$.pipe( 310 | filter((e) => e instanceof GenericError && e.error === 'login_required'), 311 | mergeMap(() => this.authService.loginWithRedirect()) 312 | ).subscribe(); 313 | } 314 | ``` 315 | 316 | ## Organizations 317 | 318 | [Organizations](https://auth0.com/docs/organizations) is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications. 319 | 320 | Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans. 321 | 322 | ### Log in to an organization 323 | 324 | Log in to an organization by specifying the `organization` parameter importing the `AuthModule`: 325 | 326 | ``` 327 | AuthModule.forRoot({ 328 | domain: 'YOUR_AUTH0_DOMAIN', 329 | clientId: 'YOUR_AUTH0_CLIENT_ID', 330 | authorizationParams: { 331 | organization: 'YOUR_ORGANIZATION_ID_OR_NAME' 332 | } 333 | }), 334 | ``` 335 | 336 | You can also specify the organization when logging in: 337 | 338 | ``` 339 | // Using a redirect 340 | this.auth.loginWithRedirect({ 341 | authorizationParams: { 342 | organization: 'YOUR_ORGANIZATION_ID_OR_NAME' 343 | } 344 | }); 345 | 346 | // Using a popup window 347 | this.auth.loginWithPopup({ 348 | authorizationParams: { 349 | organization: 'YOUR_ORGANIZATION_ID_OR_NAME' 350 | } 351 | }); 352 | ``` 353 | 354 | ### Accept user invitations 355 | 356 | Accept a user invitation through the SDK by creating a route within your application that can handle the user invitation URL, and log the user in by passing the `organization` and `invitation` parameters from this URL. You can either use `loginWithRedirect` or `loginWithPopup` as needed. 357 | 358 | ```js 359 | import { Component } from '@angular/core'; 360 | import { AuthService } from '@auth0/auth0-angular'; 361 | 362 | @Component({ 363 | selector: 'app-root', 364 | templateUrl: './app.component.html', 365 | styleUrls: ['./app.component.css'], 366 | }) 367 | export class AppComponent { 368 | constructor(public auth: AuthService, private activatedRoute: ActivatedRoute) {} 369 | 370 | loginWithRedirect(): void { 371 | const { organization, invitation } = this.activatedRoute.snapshot.params; 372 | 373 | this.auth.loginWithRedirect({ 374 | authorizationParams: { 375 | organization, 376 | invitation 377 | } 378 | }); 379 | } 380 | } 381 | ``` 382 | 383 | ## Standalone components and a more functional approach 384 | As of Angular 15, the Angular team is putting standalone components, as well as a more functional approach, in favor of the traditional use of NgModules and class-based approach. 385 | 386 | There are a couple of difference with how you would traditionally implement our SDK: 387 | 388 | - Use our functional guard (`authGuardFn`) instead of our class-based `AuthGuard`. 389 | - Use our functional interceptor (`authHttpInterceptorFn`) instead of our class-based `AuthHttpInterceptor`. 390 | - Register the interceptor by passing it to `withInterceptors` when calling `provideHttpClient`. 391 | - Register our SDK using `provideAuth0`. 392 | 393 | ```ts 394 | import { authGuardFn, authHttpInterceptorFn, provideAuth0 } from '@auth0/auth0-angular'; 395 | 396 | const routes: Routes = [ 397 | { 398 | path: 'profile', 399 | component: ProfileComponent, 400 | canActivate: [authGuardFn], 401 | } 402 | ]; 403 | 404 | bootstrapApplication(AppComponent, { 405 | providers: [ 406 | provideRouter(routes), 407 | provideAuth0(/* Auth Config Goes Here */), 408 | provideHttpClient( 409 | withInterceptors([authHttpInterceptorFn]) 410 | ) 411 | ] 412 | }); 413 | ``` 414 | 415 | Note that `provideAuth0` should **never** be provided to components, but only at the root level of your application. 416 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Auth0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Auth0 SDK for Angular Single Page Applications](https://cdn.auth0.com/website/sdks/banners/auth0-angular-banner.png) 2 | 3 | A library for integrating [Auth0](https://auth0.com) into an Angular application. 4 | 5 | ![Release](https://img.shields.io/npm/v/@auth0/auth0-angular) 6 | [![Codecov](https://img.shields.io/codecov/c/github/auth0/auth0-angular)](https://codecov.io/gh/auth0/auth0-angular) 7 | ![Downloads](https://img.shields.io/npm/dw/@auth0/auth0-angular) 8 | [![License](https://img.shields.io/:license-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) 9 | [![CircleCI](https://img.shields.io/circleci/build/github/auth0/auth0-angular)](https://circleci.com/gh/auth0/auth0-angular) 10 | 11 | 📚 [Documentation](#documentation) - 🚀 [Getting Started](#getting-started) - 💻 [API Reference](#api-reference) - 💬 [Feedback](#feedback) 12 | 13 | ## Documentation 14 | 15 | - [Quickstart](https://auth0.com/docs/quickstart/spa/angular) - our interactive guide for quickly adding login, logout and user information to an Angular app using Auth0. 16 | - [Sample App](https://github.com/auth0-samples/auth0-angular-samples/tree/master/Sample-01) - a full-fledged Angular application integrated with Auth0. 17 | - [FAQs](https://github.com/auth0/auth0-angular/tree/main/FAQ.md) - frequently asked questions about the auth0-angular SDK. 18 | - [Examples](https://github.com/auth0/auth0-angular/tree/main/EXAMPLES.md) - code samples for common Angular authentication scenario's. 19 | - [Docs site](https://www.auth0.com/docs) - explore our docs site and learn more about Auth0. 20 | 21 | ## Getting started 22 | 23 | ### Requirements 24 | 25 | This project only supports the [actively supported versions of Angular as stated in the Angular documentation](https://angular.io/guide/releases#actively-supported-versions). Whilst other versions might be compatible they are not actively supported. 26 | 27 | ### Installation 28 | 29 | Using npm: 30 | 31 | ```sh 32 | npm install @auth0/auth0-angular 33 | ``` 34 | 35 | We also have `ng-add` support, so the library can also be installed using the Angular CLI: 36 | 37 | ```sh 38 | ng add @auth0/auth0-angular 39 | ``` 40 | 41 | ### Configure Auth0 42 | 43 | Create a **Single Page Application** in the [Auth0 Dashboard](https://manage.auth0.com/#/applications). 44 | 45 | > **If you're using an existing application**, verify that you have configured the following settings in your Single Page Application: 46 | > 47 | > - Click on the "Settings" tab of your application's page. 48 | > - Scroll down and click on the "Show Advanced Settings" link. 49 | > - Under "Advanced Settings", click on the "OAuth" tab. 50 | > - Ensure that "JsonWebToken Signature Algorithm" is set to `RS256` and that "OIDC Conformant" is enabled. 51 | 52 | Next, configure the following URLs for your application under the "Application URIs" section of the "Settings" page: 53 | 54 | - **Allowed Callback URLs**: `http://localhost:4200` 55 | - **Allowed Logout URLs**: `http://localhost:4200` 56 | - **Allowed Web Origins**: `http://localhost:4200` 57 | 58 | > These URLs should reflect the origins that your application is running on. **Allowed Callback URLs** may also include a path, depending on where you're handling the callback. 59 | 60 | Take note of the **Client ID** and **Domain** values under the "Basic Information" section. You'll need these values in the next step. 61 | 62 | ### Configure the SDK 63 | 64 | #### Static configuration 65 | 66 | Install the SDK into your application by importing `AuthModule.forRoot()` and configuring with your Auth0 domain and client id, as well as the URL to which Auth0 should redirect back after succesful authentication: 67 | 68 | ```ts 69 | import { NgModule } from '@angular/core'; 70 | import { AuthModule } from '@auth0/auth0-angular'; 71 | 72 | @NgModule({ 73 | // ... 74 | imports: [ 75 | AuthModule.forRoot({ 76 | domain: 'YOUR_AUTH0_DOMAIN', 77 | clientId: 'YOUR_AUTH0_CLIENT_ID', 78 | authorizationParams: { 79 | redirect_uri: window.location.origin, 80 | }, 81 | }), 82 | ], 83 | // ... 84 | }) 85 | export class AppModule {} 86 | ``` 87 | 88 | #### Dynamic configuration 89 | 90 | Instead of using `AuthModule.forRoot` to specify auth configuration, you can provide a factory function using `APP_INITIALIZER` to load your config from an external source before the auth module is loaded, and provide your configuration using `AuthClientConfig.set`. 91 | 92 | The configuration will only be used initially when the SDK is instantiated. Any changes made to the configuration at a later moment in time will have no effect on the default options used when calling the SDK's methods. This is also the reason why the dynamic configuration should be set using an `APP_INITIALIZER`, because doing so ensures the configuration is available prior to instantiating the SDK. 93 | 94 | > :information_source: Any request made through an instance of `HttpClient` that got instantiated by Angular, will use all of the configured interceptors, including our `AuthHttpInterceptor`. Because the `AuthHttpInterceptor` requires the existence of configuration settings, the request for retrieving those dynamic configuration settings should ensure it's not using any of those interceptors. In Angular, this can be done by manually instantiating `HttpClient` using an injected `HttpBackend` instance. 95 | 96 | ```js 97 | import { AuthModule, AuthClientConfig } from '@auth0/auth0-angular'; 98 | 99 | // Provide an initializer function that returns a Promise 100 | function configInitializer( 101 | handler: HttpBackend, 102 | config: AuthClientConfig 103 | ) { 104 | return () => 105 | new HttpClient(handler) 106 | .get('/config') 107 | .toPromise() 108 | // Set the config that was loaded asynchronously here 109 | .then((loadedConfig: any) => config.set(loadedConfig)); 110 | } 111 | 112 | export class AppModule { 113 | // ... 114 | imports: [ 115 | HttpClientModule, 116 | AuthModule.forRoot(), // <- don't pass any config here 117 | ], 118 | providers: [ 119 | { 120 | provide: APP_INITIALIZER, 121 | useFactory: configInitializer, // <- pass your initializer function here 122 | deps: [HttpBackend, AuthClientConfig], 123 | multi: true, 124 | }, 125 | ], 126 | // ... 127 | } 128 | ``` 129 | 130 | ### Add login to your application 131 | 132 | To log the user into the application, inject the `AuthService` and call its `loginWithRedirect` method. 133 | 134 | ```ts 135 | import { Component } from '@angular/core'; 136 | import { AuthService } from '@auth0/auth0-angular'; 137 | 138 | @Component({ 139 | selector: 'app-root', 140 | templateUrl: './app.component.html', 141 | styleUrls: ['./app.component.css'], 142 | }) 143 | export class AppComponent { 144 | constructor(public auth: AuthService) {} 145 | 146 | loginWithRedirect() { 147 | this.auth.loginWithRedirect(); 148 | } 149 | ``` 150 | 151 | By default the application will ask Auth0 to redirect back to the root URL of your application after authentication. This can be configured by setting the [redirectUri](https://auth0.github.io/auth0-angular/interfaces/AuthorizationParams.html#redirect_uri) option. 152 | 153 | For more code samples on how to integrate the **auth0-angular** SDK in your **Angular** application, including how to use our standalone and function APIs, have a look at the [examples](https://github.com/auth0/auth0-angular/tree/main/EXAMPLES.md). 154 | 155 | ## API reference 156 | 157 | Explore public API's available in auth0-angular. 158 | 159 | - [AuthService](https://auth0.github.io/auth0-angular/classes/AuthService.html) - service used to interact with the SDK. 160 | - [AuthConfig](https://auth0.github.io/auth0-angular/interfaces/AuthConfig.html) - used to configure the SDK. 161 | 162 | ## Feedback 163 | 164 | ### Contributing 165 | 166 | We appreciate feedback and contribution to this repo! Before you get started, please see the following: 167 | 168 | - [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) 169 | - [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) 170 | 171 | ### Raise an issue 172 | 173 | To provide feedback or report a bug, please [raise an issue on our issue tracker](https://github.com/auth0/auth0-angular/issues). 174 | 175 | ### Vulnerability Reporting 176 | 177 | Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. 178 | 179 | --- 180 | 181 |

182 | 183 | 184 | 185 | Auth0 Logo 186 | 187 |

188 |

Auth0 is an easy to implement, adaptable authentication and authorization platform. To learn more checkout Why Auth0?

189 |

190 | This project is licensed under the MIT license. See the LICENSE file for more info.

191 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "auth0-angular": { 7 | "projectType": "library", 8 | "root": "projects/auth0-angular", 9 | "sourceRoot": "projects/auth0-angular/src", 10 | "prefix": "lib", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "tsConfig": "projects/auth0-angular/tsconfig.lib.json", 16 | "project": "projects/auth0-angular/ng-package.json" 17 | }, 18 | "configurations": { 19 | "production": { 20 | "tsConfig": "projects/auth0-angular/tsconfig.lib.prod.json" 21 | } 22 | } 23 | }, 24 | "test": { 25 | "builder": "@angular-devkit/build-angular:karma", 26 | "options": { 27 | "main": "projects/auth0-angular/src/test.ts", 28 | "tsConfig": "projects/auth0-angular/tsconfig.spec.json", 29 | "karmaConfig": "projects/auth0-angular/karma.conf.js" 30 | } 31 | }, 32 | "lint": { 33 | "builder": "@angular-eslint/builder:lint", 34 | "options": { 35 | "lintFilePatterns": [ 36 | "projects/auth0-angular/**/*.ts", 37 | "projects/auth0-angular/**/*.html" 38 | ] 39 | } 40 | } 41 | } 42 | }, 43 | "playground": { 44 | "projectType": "application", 45 | "schematics": {}, 46 | "root": "projects/playground", 47 | "sourceRoot": "projects/playground/src", 48 | "prefix": "app", 49 | "architect": { 50 | "build": { 51 | "builder": "@angular-devkit/build-angular:browser", 52 | "options": { 53 | "outputPath": "dist/playground", 54 | "index": "projects/playground/src/index.html", 55 | "main": "projects/playground/src/main.ts", 56 | "polyfills": "projects/playground/src/polyfills.ts", 57 | "tsConfig": "projects/playground/tsconfig.app.json", 58 | "assets": [ 59 | "projects/playground/src/favicon.ico", 60 | "projects/playground/src/assets", 61 | { 62 | "input": "projects/playground/src/config", 63 | "output": "assets/", 64 | "glob": "config.json" 65 | } 66 | ], 67 | "styles": ["projects/playground/src/styles.css"], 68 | "scripts": [], 69 | "vendorChunk": true, 70 | "extractLicenses": false, 71 | "buildOptimizer": false, 72 | "sourceMap": true, 73 | "optimization": false, 74 | "namedChunks": true 75 | }, 76 | "configurations": { 77 | "local": { 78 | "assets": [ 79 | "projects/playground/src/favicon.ico", 80 | "projects/playground/src/assets", 81 | { 82 | "input": "projects/playground/src/config/local", 83 | "output": "assets/", 84 | "glob": "config.json" 85 | } 86 | ] 87 | }, 88 | "production": { 89 | "fileReplacements": [ 90 | { 91 | "replace": "projects/playground/src/environments/environment.ts", 92 | "with": "projects/playground/src/environments/environment.prod.ts" 93 | } 94 | ], 95 | "optimization": true, 96 | "outputHashing": "all", 97 | "sourceMap": false, 98 | "namedChunks": false, 99 | "extractLicenses": true, 100 | "vendorChunk": false, 101 | "buildOptimizer": true, 102 | "budgets": [ 103 | { 104 | "type": "initial", 105 | "maximumWarning": "2mb", 106 | "maximumError": "5mb" 107 | }, 108 | { 109 | "type": "anyComponentStyle", 110 | "maximumWarning": "6kb", 111 | "maximumError": "10kb" 112 | } 113 | ] 114 | } 115 | } 116 | }, 117 | "serve": { 118 | "builder": "@angular-devkit/build-angular:dev-server", 119 | "options": { 120 | "browserTarget": "playground:build", 121 | "host": "0.0.0.0" 122 | }, 123 | "configurations": { 124 | "production": { 125 | "browserTarget": "playground:build" 126 | }, 127 | "local": { 128 | "browserTarget": "playground:build:local", 129 | "proxyConfig": "projects/playground/src/proxy.conf.json" 130 | } 131 | } 132 | }, 133 | "extract-i18n": { 134 | "builder": "@angular-devkit/build-angular:extract-i18n", 135 | "options": { 136 | "browserTarget": "playground:build" 137 | } 138 | }, 139 | "test": { 140 | "builder": "@angular-devkit/build-angular:karma", 141 | "options": { 142 | "main": "projects/playground/src/test.ts", 143 | "polyfills": "projects/playground/src/polyfills.ts", 144 | "tsConfig": "projects/playground/tsconfig.spec.json", 145 | "karmaConfig": "projects/playground/karma.conf.js", 146 | "assets": [ 147 | "projects/playground/src/favicon.ico", 148 | "projects/playground/src/assets" 149 | ], 150 | "styles": ["projects/playground/src/styles.css"], 151 | "scripts": [] 152 | } 153 | }, 154 | "e2e": { 155 | "builder": "@cypress/schematic:cypress", 156 | "options": { 157 | "devServerTarget": "playground:serve:local", 158 | "watch": true, 159 | "headless": false 160 | }, 161 | "configurations": { 162 | "production": { 163 | "devServerTarget": "playground:serve:production" 164 | } 165 | } 166 | }, 167 | "cypress-run": { 168 | "builder": "@cypress/schematic:cypress", 169 | "options": { 170 | "devServerTarget": "playground:serve" 171 | }, 172 | "configurations": { 173 | "production": { 174 | "devServerTarget": "playground:serve:production" 175 | } 176 | } 177 | }, 178 | "cypress-open": { 179 | "builder": "@cypress/schematic:cypress", 180 | "options": { 181 | "devServerTarget": "playground:serve", 182 | "watch": true, 183 | "headless": false 184 | }, 185 | "configurations": { 186 | "production": { 187 | "devServerTarget": "playground:serve:production" 188 | } 189 | } 190 | }, 191 | "lint": { 192 | "builder": "@angular-eslint/builder:lint", 193 | "options": { 194 | "lintFilePatterns": [ 195 | "projects/playground/**/*.ts", 196 | "projects/playground/**/*.html" 197 | ] 198 | } 199 | } 200 | } 201 | } 202 | }, 203 | "defaultProject": "auth0-angular", 204 | "cli": { 205 | "defaultCollection": "@angular-eslint/schematics" 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /api-server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const { expressjwt: jwt } = require('express-jwt'); 4 | const jwksRsa = require('jwks-rsa'); 5 | 6 | const app = express(); 7 | 8 | const authConfig = { 9 | domain: 'brucke.auth0.com', 10 | audience: 'http://localhost/', 11 | appUri: 'http://localhost:4200', 12 | }; 13 | 14 | if (!authConfig.domain || !authConfig.audience) { 15 | throw 'Please make sure that auth_config.json is in place and populated'; 16 | } 17 | 18 | app.use( 19 | cors({ 20 | origin: authConfig.appUri, 21 | }) 22 | ); 23 | 24 | const checkJwt = jwt({ 25 | secret: jwksRsa.expressJwtSecret({ 26 | cache: true, 27 | rateLimit: true, 28 | jwksRequestsPerMinute: 5, 29 | jwksUri: `https://${authConfig.domain}/.well-known/jwks.json`, 30 | }), 31 | 32 | audience: authConfig.audience, 33 | issuer: `https://${authConfig.domain}/`, 34 | algorithms: ['RS256'], 35 | }); 36 | 37 | app.get('/api/external', checkJwt, (req, res) => { 38 | res.send({ 39 | msg: 'Your access token was successfully validated!', 40 | }); 41 | }); 42 | 43 | const port = process.env.API_SERVER_PORT || 3001; 44 | 45 | app.listen(port, () => console.log(`Api started on port ${port}`)); 46 | -------------------------------------------------------------------------------- /browserstack.json: -------------------------------------------------------------------------------- 1 | { 2 | "browsers": [ 3 | { 4 | "browser": "chrome", 5 | "os": "Windows 10", 6 | "versions": ["latest"] 7 | }, 8 | { 9 | "browser": "firefox", 10 | "os": "Windows 10", 11 | "versions": ["latest"] 12 | }, 13 | { 14 | "browser": "edge", 15 | "os": "Windows 10", 16 | "versions": ["latest"] 17 | } 18 | ], 19 | "run_settings": { 20 | "cypress_config_file": "./cypress.json", 21 | "cypress-version": "9", 22 | "project_name": "Auth0 Angular SDK", 23 | "exclude": [], 24 | "parallels": "5", 25 | "npm_dependencies": { 26 | "typescript": "~4.2.4", 27 | "qss": "2.0.3" 28 | }, 29 | "package_config_options": {}, 30 | "headless": true 31 | }, 32 | "connection_settings": { 33 | "local": true, 34 | "local_mode": "always-on" 35 | }, 36 | "disable_usage_reporting": false 37 | } 38 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | ignore: 3 | - 'projects/playground' 4 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://127.0.0.1:4200", 3 | "chromeWebSecurity": false, 4 | "viewportWidth": 1000, 5 | "viewportHeight": 1000, 6 | "fixturesFolder": false, 7 | "pluginsFile": false, 8 | "supportFile": false, 9 | "reporter": "junit", 10 | "reporterOptions": { 11 | "mochaFile": "projects/playground/test-results/e2e/junit-[hash].xml" 12 | }, 13 | "fileServerFolder": "projects/playground", 14 | "integrationFolder": "projects/playground/e2e/integration", 15 | "pluginFolder": "projects/playground/e2e/plugins", 16 | "screenshotsFolder": "projects/playground/e2e/screenshots", 17 | "videosFolder": "projects/playground/e2e/videos" 18 | } 19 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #795e26; 3 | --dark-hl-0: #dcdcaa; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #d4d4d4; 6 | --light-hl-2: #a31515; 7 | --dark-hl-2: #ce9178; 8 | --light-hl-3: #af00db; 9 | --dark-hl-3: #c586c0; 10 | --light-hl-4: #001080; 11 | --dark-hl-4: #9cdcfe; 12 | --light-hl-5: #008000; 13 | --dark-hl-5: #6a9955; 14 | --light-hl-6: #0000ff; 15 | --dark-hl-6: #569cd6; 16 | --light-hl-7: #267f99; 17 | --dark-hl-7: #4ec9b0; 18 | --light-hl-8: #000000; 19 | --dark-hl-8: #c8c8c8; 20 | --light-hl-9: #0070c1; 21 | --dark-hl-9: #4fc1ff; 22 | --light-code-background: #ffffff; 23 | --dark-code-background: #1e1e1e; 24 | } 25 | 26 | @media (prefers-color-scheme: light) { 27 | :root { 28 | --hl-0: var(--light-hl-0); 29 | --hl-1: var(--light-hl-1); 30 | --hl-2: var(--light-hl-2); 31 | --hl-3: var(--light-hl-3); 32 | --hl-4: var(--light-hl-4); 33 | --hl-5: var(--light-hl-5); 34 | --hl-6: var(--light-hl-6); 35 | --hl-7: var(--light-hl-7); 36 | --hl-8: var(--light-hl-8); 37 | --hl-9: var(--light-hl-9); 38 | --code-background: var(--light-code-background); 39 | } 40 | } 41 | 42 | @media (prefers-color-scheme: dark) { 43 | :root { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --hl-5: var(--dark-hl-5); 50 | --hl-6: var(--dark-hl-6); 51 | --hl-7: var(--dark-hl-7); 52 | --hl-8: var(--dark-hl-8); 53 | --hl-9: var(--dark-hl-9); 54 | --code-background: var(--dark-code-background); 55 | } 56 | } 57 | 58 | :root[data-theme='light'] { 59 | --hl-0: var(--light-hl-0); 60 | --hl-1: var(--light-hl-1); 61 | --hl-2: var(--light-hl-2); 62 | --hl-3: var(--light-hl-3); 63 | --hl-4: var(--light-hl-4); 64 | --hl-5: var(--light-hl-5); 65 | --hl-6: var(--light-hl-6); 66 | --hl-7: var(--light-hl-7); 67 | --hl-8: var(--light-hl-8); 68 | --hl-9: var(--light-hl-9); 69 | --code-background: var(--light-code-background); 70 | } 71 | 72 | :root[data-theme='dark'] { 73 | --hl-0: var(--dark-hl-0); 74 | --hl-1: var(--dark-hl-1); 75 | --hl-2: var(--dark-hl-2); 76 | --hl-3: var(--dark-hl-3); 77 | --hl-4: var(--dark-hl-4); 78 | --hl-5: var(--dark-hl-5); 79 | --hl-6: var(--dark-hl-6); 80 | --hl-7: var(--dark-hl-7); 81 | --hl-8: var(--dark-hl-8); 82 | --hl-9: var(--dark-hl-9); 83 | --code-background: var(--dark-code-background); 84 | } 85 | 86 | .hl-0 { 87 | color: var(--hl-0); 88 | } 89 | .hl-1 { 90 | color: var(--hl-1); 91 | } 92 | .hl-2 { 93 | color: var(--hl-2); 94 | } 95 | .hl-3 { 96 | color: var(--hl-3); 97 | } 98 | .hl-4 { 99 | color: var(--hl-4); 100 | } 101 | .hl-5 { 102 | color: var(--hl-5); 103 | } 104 | .hl-6 { 105 | color: var(--hl-6); 106 | } 107 | .hl-7 { 108 | color: var(--hl-7); 109 | } 110 | .hl-8 { 111 | color: var(--hl-8); 112 | } 113 | .hl-9 { 114 | color: var(--hl-9); 115 | } 116 | pre, 117 | code { 118 | background: var(--code-background); 119 | } 120 | -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = 2 | 'data:application/octet-stream;base64,H4sIAAAAAAAAE6WWQXPTMBCF/4vPKWkDLdBbJ9CSmQY6SRkODAdVXsc7dSQjyRkCw39Hlksiy5Lsca7ep+9Zq/Wzvv9JFPxSyXXySalyCSrnaTJJSqJy/QxYtZXTY+VVrraFLj8jS5Prd38nh9U3T1IJQtVnssMNUVwcIbQgUoKcdiRt2sWsxatUfj4vEJi61Wu42HuAHU0fsRHPOctw4+fZij7aXUVE6seYUt/6uq8LpkBQKP0d64r6mEueVgX4UU2tj7AGsUMaQLwUexmKqBChLvWt10eAlCjk7KMQoc44og5z0qj1KiXTM5Rnmg+CkSKx3O6AgUAasLGr4/gLtoStnsw5obmnI63yOId7Tkmx1qNBNhBw6UjGOS0zsoKfFQpIA/1yFSN9UEpkmxVkAmT+yJ8hNAYh5TjfB15W5ZwwCkUR3KJHdILbI26BVyrmZUvGOcVNTud/leDh1k/H8W7K0gkQrAMwI7T+8l+KbfTs8sqNeifkbcSh3AfhAn+bhHkggmxlkOboOtiBUaTMBK+x0NFW7L+UNdFvGtCeZvwNVW5GboizKx5n7fzdIofmVcbOz1mw0jMOw/mWPGaycBLXAi48STu0LYvU9NjPbWrjwPd8ozcWO9+WIrb1JghNh2K8rmzcmxuOfjlkvW62apzZClL9D6Oq188njIZKiWayPkCGDGv5Eav2pck3V+Hwzt+/vbicWUwzZ+TJvvU1qEPBSxgWxccrdudeuCMCa3rrIu69IL6eecO5h9gSRYDk/3371mpmVjFqTmNqlduQqzcOxImAMK4jjIBRDgmiIz+mj9iUgu8wBXMSPqxd72B+/AN1KrDXBg4AAA=='; 3 | -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = 2 | 'data:application/octet-stream;base64,H4sIAAAAAAAAE61b227bSBL9F3kfDcfsC2n7LZvszARIdgbJXh6CIGCotkWEJgmScjYw8u/b3STFKqqKakl+MmSdunSdqq6+6XnVVD/a1d3n59X3vFyv7iJxc7kq00ezulu93nabT6Z5yjOzulxtm8L+LyvStjXtK/Dd1aZ7LCxg+MqCVr8uR306Ejt9WVW2XbPNuqo5qO8Cg4Huy1WdNqbsZg5OJqNroXY28/Z9la7z8uFvh03usF/Ps+hAFp5naWfWQXaRxFnWt61pAkw62HmjXP+r+m7KN0WaP7YhY4T4syybpqlChuhxZ1lK6/pTZxkJMDZCj7UnrtVUb+XDn+VbY7O++nnYIgafbLOoHvLyv3m3+WjWeWOy7rBlSuR8+39V9bY+wviIP8dytQ0bbo872dKD6V5nmWlbXwKf8sJKFgEUs3Iv5MkRYecFT/Zlk5brwow59CYtim9p9v2wK6zcUXU+a3MfqvW2oLtc/9Vik0Pjuq+aj1VFJxbQdTHheMcHt05rp9BYaDfdMzgP1O/btFmT5vw3L7YWmLQF+967xrCSpaVr7gdt7WCn23mddfmT7QUBtgD0fHtvNnkRMsA5PtzyPBn+6Lr6XdmZJjM1R+QM82IJQukNTpW540xw8xES7gEUOcV+DBeSM9hH24fMm6q8zx92Dt1vS0unHfarJfhi1KfxOgUfTLeppjQy5faxfTV9sagpineqfjfdso6LHkHHCDhCav+rag+pHyAn6t8eVL+wJDioPe2yzSH9A+Y0C29NYcAERJvYgU6z8YdJD6TJxQA5Qr+4vk0iPc0Cr+vcp/Fbc5+XuUv0ncnuZ+0KcQ+w3Kl1jLIdlMussHwh36d2zfOKBC4XAtpGFEX1w6zf5yBng7RfYEk+jPujCBoxNZ/wjoVOJ3g72uQnqL7oxYIGDAfBO/HBlZNpTvRlkj7DpVhrOVHx9avL3/P8udopOdatKzAoJmidW+j/WfvecoqbM/mX4nKz36KOcApJv5RLvkpfl1X587HanhSrPQ1nuAbr3S01+Pqevg2v5/Z7XrNbNl73BSPHr5AOZwG18FzwYF/kVOP+YMn2502IWQg+wiBicThbIs0N34Uz2KXNgyEbEdJ1scMxXo9OTWakIBJvfn78lDZ5+q1AkQo5R55vPd4UuXVkltxwdQ4BL7bp2FMavONA/jLbjdbQG419q+0SM4HWHoKtLeYBb21O2XWP+y11waKPvzAk/Mwla4zb0HrhUMUXMyF+fDPHT0sd0oPA5GEdmJXcgFuuOQw6qujwNIRO6A5OQkddBE3TUPA10GwyOuoSaDIXegW0bC3kAgjaPOb6Z8nywuXPZC7g6mdxdAEXP2BsR1z7LFlduvSZrIVc+cyszCfgd2MCHLI1w55osTH3jWk3h4xNsNNHBs7vA8aG0adb/YejJMDeiDvCElgijUvL9+52aGHTQuGCzwve+4ugBe0IsKgWnPLVTfWUr42fmYlTPfh1qMp0PLr9rSQ0gm+PUTjbfLCq93DhHeabnelti/tn+pQ/pGRvmCNepuOQWsM6z57L3NVuDzB///nvpgj1YC4U7MOXS5uWa/O/1d3z6sk0rTs2u1uJK3l1ayXvc1Os3ZuP3jmrt3p8dLq+DN/9x7hBO0QPeWUT8/P1pby9itWXL5efRwH/f/8Pj4rsp+hSiCtxi2ERggn7SVxKdaWlQDCBYNJ+khRMIpiynxQFUwim7SdNwTSCxfZTTMFiBEvsp4SCJQh2Yz/dEGG7QShLyedbAnWLg+tiHVEkRDMWPA0RBcQ8RC7etkQIIGYichGPJAXEXEQu5pGigJiNyEU90hQQ8xG5uEcxBcSMRAmbfJiTyAU/SiiNmJboltWImRGeGYpngZkRnhmKajGrEF8iFNcCMyNc/AXFtcDMCMUNRmBmhIu/oJJCYGaEi7+gkkJgZoSLvyBLVGBqhCNAkFUqMDfCMSCotBCYG+kYEBTbEnMjHQOCIlFibqTnhiJRzuYvP4FRJErMjXQMSIpEibmRjgFJcSMxN9IxICluJOZGOgIkVbASUyNd/CVVsBIzI138JcWMxMwoF39JMaMwM8rFX1LMKMyMEmyiKUyN8tRQHKpZc/HdheJQYWqUI0BRHCpMjXIEKIpDhalRjgBFcagwNeqGa0YKU6McAYoiW2FqtCNAUWRrTI12BCiKbI2p0YKbfjRmRrv4KyorNGZGK3au0LPO76mh8kdjarSnhsoKjanRbK/RmBrtCNBU+mhMjXYEaCp9NKYmvuZMx5iaOOJWWTGmJhbcOivG1MSSW2nFmJpYcWutGDMTa3a1NVuVufhrqmhizEzs4q+pookxM7FnhqqFGDMTe2aoWogxM4mLv6ZqIcHMJC7+mkrxBDOTuPhrKnETzEzi4q+pxE0wM4mLf0zlY4KZSTwzVD4mmJkk5vIxma2YXfxjisKkZ8ZvWexepTPrd/3WxW4+xvvN59XXYT9jO8awpXpe2UZx9/zr17R/cZ+c6nTYEZXTRnLSYIe102BHw2hwF3LpdCEHHLgFDsglcbMu/L09kJWTrO3EjGydN+5ibw1ePAAVwHu7POBU1G1/UjsJgrglDni5su2BEXdnDZk/JL4fz+onRdOm9nml2fhNKtrxMHpSYafEScWSF72GbLjkAT7EQEG0pGBfFNhWbAB3ooT3tjNOxtkUtBoe+qeSk2QESIiWxu0l7xHxGuRdwuadlXX3nDm855x0CJA84ggd2JMYRDBZiuDj8KYWBACELlrKHSLswOpS0PcSX4NB225KS2Zpme6eYgJ/QdAFl2dANutfVIKIA6/t3o1VUPg3p8AwKDLBDRcdUAFZEOO+0qOh4qPb/q9Q/V8thr/D9/F1/zfh5qb+xiwbbsxASYA4aU54PTw3A9EBYpKLjukPhkH+TULx4DeXSl629pfvwFkwC6uEkbyvrGiFBhkBwxE3SH9VCkYIEl7cDNHmasbKpv7U3T+daXfP/YELArjABQyr+ZF3m7p/qg/0gBBE3ETQv7NvhhPzbPdoA6hRQA03qo3BqS1BSUjWNj+JKRADxdEwk99vAxJokVz6zLT4tkyoAlGQXD45VY/DwyOQHkBUjG2Z4zVfe0Izf5WGZjhQfENBcGSA18nACxALwcUib1N4SYkCAMY/2GfH0IbFVACdgiM5b4vx+g1IAsHBGW7i9j8lousD5GjETb878Wb3M6hJA5jZuJ4z/q4ImIVT94LXVqwab6FAGsAlHic9Xil8++luIOCsCgKXcFlcPlQWPfzmbJIFzYpb1dT9c2bAExCSnLd1hRfQAkzBgovrcGOW9hdqID7AYsIlaI0ZESCmkkuEkX+fEBQxwOuEVzLc8AI5UAQxR0mLG44CDmtudm1Rh0AmwYQUc1xa8b2mrMEkFHPMWEGyZjXIvZirdveCcKkZKVA8ipvFxkdtQAyMWHGO+zgRzEqQUopLYv+WGciASEkuxFbmcXxzDERBIknOV/e8Aw0QuDhMiGRe2A14ndemyEuL/fzl16//Awp3ycYbPgAA'; 3 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | projects: [ 3 | '/projects/auth0-angular', 4 | '/projects/playground', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /opslevel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | repository: 4 | owner: dx_sdks 5 | tier: 6 | tags: 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0-angular", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "start:local": "concurrently --raw --kill-others --success first npm:start:local:oidc npm:start:local:playground", 8 | "start:local:oidc": "node scripts/oidc-provider", 9 | "start:local:playground": "ng serve --configuration=local", 10 | "prebuild": "npm run update-useragent", 11 | "build:dev": "ng build && npm run build:schematics", 12 | "build": "ng build --configuration=production && npm run build:schematics", 13 | "build:schematics": "npm run build --prefix projects/auth0-angular", 14 | "postbuild": "cp README.md ./dist/auth0-angular/", 15 | "test": "jest", 16 | "test:ci": "jest --coverage --silent", 17 | "e2e": "ng e2e", 18 | "e2e:headless": "ng e2e --no-watch --headless", 19 | "e2e:ci": "concurrently --raw --kill-others --success first npm:start:local:oidc npm:e2e:headless", 20 | "lint": "ng lint", 21 | "docs": "typedoc", 22 | "pretty-quick": "pretty-quick", 23 | "update-useragent": "node projects/auth0-angular/scripts/update-useragent.js", 24 | "server:api": "node api-server.js" 25 | }, 26 | "private": true, 27 | "dependencies": { 28 | "@angular/animations": "^13.4.0", 29 | "@angular/common": "^13.4.0", 30 | "@angular/compiler": "^13.4.0", 31 | "@angular/core": "^13.4.0", 32 | "@angular/platform-browser": "^13.4.0", 33 | "@angular/platform-browser-dynamic": "^13.4.0", 34 | "@angular/router": "^13.4.0", 35 | "@auth0/auth0-spa-js": "^2.1.3", 36 | "rxjs": "^6.6.7", 37 | "tslib": "^2.6.3", 38 | "zone.js": "~0.11.8" 39 | }, 40 | "devDependencies": { 41 | "@angular-devkit/build-angular": "^13.3.11", 42 | "@angular-eslint/builder": "13.5.0", 43 | "@angular-eslint/eslint-plugin": "13.5.0", 44 | "@angular-eslint/eslint-plugin-template": "13.5.0", 45 | "@angular-eslint/schematics": "13.5.0", 46 | "@angular-eslint/template-parser": "13.5.0", 47 | "@angular/cli": "^13.3.11", 48 | "@angular/compiler-cli": "^13.4.0", 49 | "@angular/forms": "^13.4.0", 50 | "@cypress/schematic": "^2.4.0", 51 | "@types/jasmine": "^3.10.18", 52 | "@types/jasminewd2": "~2.0.13", 53 | "@types/jest": "^29.2.2", 54 | "@types/node": "^12.19.8", 55 | "@typescript-eslint/eslint-plugin": "5.62.0", 56 | "@typescript-eslint/parser": "5.62.0", 57 | "browserstack-cypress-cli": "^1.31.1", 58 | "concurrently": "^6.2.0", 59 | "cors": "^2.8.5", 60 | "cypress": "^13.12.0", 61 | "eslint": "^8.57.0", 62 | "eslint-plugin-import": "latest", 63 | "eslint-plugin-jsdoc": "latest", 64 | "eslint-plugin-prefer-arrow": "latest", 65 | "express-jwt": "^8.4.1", 66 | "husky": "^4.3.8", 67 | "jest": "^28.1.3", 68 | "jest-preset-angular": "^12.2.6", 69 | "jwks-rsa": "^3.1.0", 70 | "moment": "^2.30.1", 71 | "ng-packagr": "^13.3.1", 72 | "oidc-provider": "^7.6.0", 73 | "prettier": "^2.8.8", 74 | "pretty-quick": "^2.0.2", 75 | "protractor": "~7.0.0", 76 | "ts-node": "~8.10.2", 77 | "typedoc": "^0.25.13", 78 | "typescript": "~4.6.4" 79 | }, 80 | "prettier": { 81 | "semi": true, 82 | "singleQuote": true 83 | }, 84 | "husky": { 85 | "hooks": { 86 | "pre-commit": "pretty-quick --staged" 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /projects/auth0-angular/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": [ 9 | "projects/auth0-angular/tsconfig.lib.json", 10 | "projects/auth0-angular/tsconfig.spec.json" 11 | ], 12 | "createDefaultProgram": true 13 | }, 14 | "rules": { 15 | "@angular-eslint/component-selector": [ 16 | "error", 17 | { 18 | "type": "element", 19 | "prefix": "lib", 20 | "style": "kebab-case" 21 | } 22 | ], 23 | "@angular-eslint/directive-selector": [ 24 | "error", 25 | { 26 | "type": "attribute", 27 | "prefix": "lib", 28 | "style": "camelCase" 29 | } 30 | ] 31 | } 32 | }, 33 | { 34 | "files": ["*.html"], 35 | "rules": {} 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /projects/auth0-angular/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'auth0-angular', 4 | preset: 'jest-preset-angular', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: { 7 | 'ts-jest': { 8 | tsconfig: '/tsconfig.spec.json', 9 | stringifyContentPathRegex: '\\.(html|svg)$', 10 | }, 11 | }, 12 | coverageDirectory: '../../coverage/auth0-angular', 13 | transform: { 14 | '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', 15 | }, 16 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 17 | snapshotSerializers: [ 18 | 'jest-preset-angular/build/serializers/no-ng-attributes', 19 | 'jest-preset-angular/build/serializers/ng-snapshot', 20 | 'jest-preset-angular/build/serializers/html-comment', 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /projects/auth0-angular/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-junit-reporter'), 13 | require('karma-coverage'), 14 | require('@angular-devkit/build-angular/plugins/karma'), 15 | ], 16 | client: { 17 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 18 | }, 19 | coverageReporter: { 20 | dir: require('path').join(__dirname, './../../coverage/auth0-angular'), 21 | subdir: '.', 22 | reporters: [ 23 | { type: 'html' }, 24 | { type: 'lcovonly' }, 25 | { type: 'text-summary' }, 26 | ], 27 | }, 28 | reporters: ['progress', 'kjhtml', 'junit'], 29 | port: 9876, 30 | colors: true, 31 | logLevel: config.LOG_INFO, 32 | autoWatch: true, 33 | browsers: ['Chrome'], 34 | customLaunchers: { 35 | ChromeHeadlessCI: { 36 | base: 'ChromeHeadless', 37 | flags: ['--no-sandbox'], 38 | }, 39 | }, 40 | singleRun: false, 41 | restartOnFileChange: true, 42 | junitReporter: { 43 | outputDir: 'test-results', 44 | }, 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /projects/auth0-angular/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/auth0-angular", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | }, 7 | "allowedNonPeerDependencies": ["tslib", "@auth0/auth0-spa-js"] 8 | } 9 | -------------------------------------------------------------------------------- /projects/auth0-angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@auth0/auth0-angular", 3 | "version": "2.2.3", 4 | "description": "Auth0 SDK for Angular Single Page Applications (SPA)", 5 | "keywords": [ 6 | "auth0", 7 | "login", 8 | "Authorization Code Grant Flow", 9 | "PKCE", 10 | "Single Page Application authentication", 11 | "SPA authentication", 12 | "angular" 13 | ], 14 | "scripts": { 15 | "build": "tsc -p tsconfig.schematics.json", 16 | "copy:collection": "cp schematics/collection.json ../../dist/auth0-angular/schematics/collection.json", 17 | "postbuild": "npm run copy:collection" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/auth0/auth0-angular.git" 22 | }, 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/auth0/auth0-angular/issues" 26 | }, 27 | "homepage": "https://github.com/auth0/auth0-angular#readme", 28 | "peerDependencies": { 29 | "@angular/common": ">=13", 30 | "@angular/core": ">=13", 31 | "@angular/router": ">=13" 32 | }, 33 | "dependencies": { 34 | "tslib": "^2.0.0", 35 | "@auth0/auth0-spa-js": "^2.0.1" 36 | }, 37 | "schematics": "./schematics/collection.json" 38 | } 39 | -------------------------------------------------------------------------------- /projects/auth0-angular/schematics/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "schematics": { 4 | "ng-add": { 5 | "description": "Add Auth0 Angular to the project.", 6 | "factory": "./ng-add/index#ngAdd" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/auth0-angular/schematics/ng-add/index.ts: -------------------------------------------------------------------------------- 1 | import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; 2 | import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; 3 | 4 | // Just return the tree 5 | export function ngAdd(): Rule { 6 | return (tree: Tree, context: SchematicContext) => { 7 | context.addTask(new NodePackageInstallTask()); 8 | return tree; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /projects/auth0-angular/scripts/update-useragent.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const pkg = require('../package.json'); 3 | 4 | console.log( 5 | `Updating the "useragent.ts" file using name=${pkg.name} and version=${pkg.version}` 6 | ); 7 | fs.writeFileSync( 8 | 'projects/auth0-angular/src/useragent.ts', 9 | `export default { name: '${pkg.name}', version: '${pkg.version}' }; 10 | ` 11 | ); 12 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/abstract-navigator.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, fakeAsync, tick } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { Location } from '@angular/common'; 4 | import { AbstractNavigator } from './abstract-navigator'; 5 | import { Component } from '@angular/core'; 6 | 7 | describe('RouteNavigator', () => { 8 | let navigator: AbstractNavigator; 9 | let replaceStateSpy: any; 10 | 11 | // Stub component for the sake of getting the router to accept routes 12 | @Component({}) 13 | class StubComponent {} 14 | 15 | describe('with no router', () => { 16 | beforeEach(() => { 17 | TestBed.configureTestingModule({}); 18 | 19 | navigator = TestBed.inject(AbstractNavigator); 20 | 21 | const location = TestBed.inject(Location); 22 | replaceStateSpy = jest.spyOn(location, 'replaceState'); 23 | }); 24 | 25 | it('should be created', () => { 26 | expect(navigator).toBeTruthy(); 27 | }); 28 | 29 | it('should use the window object when navigating', async () => { 30 | await navigator.navigateByUrl('/test-url'); 31 | 32 | expect(replaceStateSpy).toHaveBeenCalledWith('/test-url'); 33 | }); 34 | }); 35 | 36 | describe('with a router', () => { 37 | beforeEach(() => { 38 | TestBed.configureTestingModule({ 39 | imports: [ 40 | RouterTestingModule.withRoutes([ 41 | { 42 | path: 'test-route', 43 | component: StubComponent, 44 | }, 45 | ]), 46 | ], 47 | }); 48 | 49 | navigator = TestBed.inject(AbstractNavigator); 50 | 51 | const location = TestBed.inject(Location); 52 | replaceStateSpy = jest.spyOn(location, 'replaceState'); 53 | }); 54 | 55 | it('should use the router if available', fakeAsync(() => { 56 | const location = TestBed.inject(Location); 57 | navigator.navigateByUrl('/test-route'); 58 | tick(); 59 | expect(location.path()).toBe('/test-route'); 60 | })); 61 | 62 | it('should not use the window object to navigate', async () => { 63 | expect(replaceStateSpy).not.toHaveBeenCalled(); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/abstract-navigator.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { Location } from '@angular/common'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class AbstractNavigator { 9 | private readonly router?: Router; 10 | 11 | constructor(private location: Location, injector: Injector) { 12 | try { 13 | this.router = injector.get(Router); 14 | } catch {} 15 | } 16 | 17 | /** 18 | * Navigates to the specified url. The router will be used if one is available, otherwise it falls back 19 | * to `window.history.replaceState`. 20 | * 21 | * @param url The url to navigate to 22 | */ 23 | navigateByUrl(url: string): void { 24 | if (this.router) { 25 | this.router.navigateByUrl(url); 26 | 27 | return; 28 | } 29 | 30 | this.location.replaceState(url); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.client.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig, AuthClientConfig } from './auth.config'; 2 | import { Auth0ClientFactory } from './auth.client'; 3 | 4 | const mockWindow = global as any; 5 | 6 | mockWindow.crypto = { 7 | subtle: { 8 | digest: () => 'foo', 9 | }, 10 | getRandomValues() { 11 | return '123'; 12 | }, 13 | }; 14 | 15 | describe('Auth0ClientFactory', () => { 16 | describe('createClient', () => { 17 | it('creates a new instance of Auth0Client', () => { 18 | const config: AuthConfig = { 19 | domain: 'test.domain.com', 20 | clientId: 'abc123', 21 | }; 22 | 23 | const configClient = new AuthClientConfig(config); 24 | const client = Auth0ClientFactory.createClient(configClient); 25 | 26 | expect(client).not.toBeUndefined(); 27 | }); 28 | 29 | it('throws an error when no config was supplied', () => { 30 | const configClient = new AuthClientConfig(); 31 | 32 | expect(() => Auth0ClientFactory.createClient(configClient)).toThrowError( 33 | /^Configuration must be specified/ 34 | ); 35 | }); 36 | 37 | it('creates a new instance of Auth0Client with the correct properties to skip the refreshtoken fallback', () => { 38 | const config: AuthConfig = { 39 | domain: 'test.domain.com', 40 | clientId: 'abc123', 41 | useRefreshTokens: true, 42 | useRefreshTokensFallback: false, 43 | }; 44 | 45 | const configClient = new AuthClientConfig(config); 46 | const client = Auth0ClientFactory.createClient(configClient); 47 | 48 | expect(client).not.toBeUndefined(); 49 | expect((client as any).options.domain).toEqual('test.domain.com'); 50 | expect((client as any).options.clientId).toEqual('abc123'); 51 | expect((client as any).options.useRefreshTokens).toEqual(true); 52 | expect((client as any).options.useRefreshTokensFallback).toEqual(false); 53 | }); 54 | 55 | it('creates a new instance of Auth0Client with the correct properties without any value for useRefreshTokensFallback', () => { 56 | const config: AuthConfig = { 57 | domain: 'test.domain.com', 58 | clientId: 'abc123', 59 | useRefreshTokens: true, 60 | }; 61 | 62 | const configClient = new AuthClientConfig(config); 63 | const client = Auth0ClientFactory.createClient(configClient); 64 | 65 | expect(client).not.toBeUndefined(); 66 | expect((client as any).options.domain).toEqual('test.domain.com'); 67 | expect((client as any).options.clientId).toEqual('abc123'); 68 | expect((client as any).options.useRefreshTokens).toEqual(true); 69 | expect((client as any).options.useRefreshTokensFallback).toEqual(false); 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.client.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, VERSION } from '@angular/core'; 2 | import { Auth0Client } from '@auth0/auth0-spa-js'; 3 | import { AuthClientConfig } from './auth.config'; 4 | import useragent from '../useragent'; 5 | 6 | export class Auth0ClientFactory { 7 | static createClient(configFactory: AuthClientConfig): Auth0Client { 8 | const config = configFactory.get(); 9 | 10 | if (!config) { 11 | throw new Error( 12 | 'Configuration must be specified either through AuthModule.forRoot or through AuthClientConfig.set' 13 | ); 14 | } 15 | 16 | return new Auth0Client({ 17 | ...config, 18 | auth0Client: { 19 | name: useragent.name, 20 | version: useragent.version, 21 | env: { 22 | 'angular/core': VERSION.full, 23 | }, 24 | }, 25 | }); 26 | } 27 | } 28 | 29 | export const Auth0ClientService = new InjectionToken( 30 | 'auth0.client' 31 | ); 32 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.config.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthClientConfig, AuthConfig } from './auth.config'; 2 | 3 | describe('AuthClientConfig', () => { 4 | const testConfig: AuthConfig = { 5 | domain: 'test.domain.com', 6 | clientId: '123abc', 7 | }; 8 | 9 | it('caches the config as given through the constructor', () => { 10 | const config = new AuthClientConfig(testConfig); 11 | expect(config.get()).toBe(testConfig); 12 | }); 13 | 14 | it('caches the config as given through the setter', () => { 15 | const config = new AuthClientConfig(); 16 | config.set(testConfig); 17 | expect(config.get()).toBe(testConfig); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Auth0ClientOptions, 3 | CacheLocation, 4 | GetTokenSilentlyOptions, 5 | ICache, 6 | } from '@auth0/auth0-spa-js'; 7 | 8 | import { InjectionToken, Injectable, Optional, Inject } from '@angular/core'; 9 | 10 | /** 11 | * Defines a common set of HTTP methods. 12 | */ 13 | export const enum HttpMethod { 14 | Get = 'GET', 15 | Post = 'POST', 16 | Put = 'PUT', 17 | Patch = 'PATCH', 18 | Delete = 'DELETE', 19 | Head = 'HEAD', 20 | } 21 | 22 | /** 23 | * Defines the type for a route config entry. Can either be: 24 | * 25 | * - an object of type HttpInterceptorRouteConfig 26 | * - a string 27 | */ 28 | export type ApiRouteDefinition = HttpInterceptorRouteConfig | string; 29 | 30 | /** 31 | * A custom type guard to help identify route definitions that are actually HttpInterceptorRouteConfig types. 32 | * 33 | * @param def The route definition type 34 | */ 35 | export function isHttpInterceptorRouteConfig( 36 | def: ApiRouteDefinition 37 | ): def is HttpInterceptorRouteConfig { 38 | return typeof def !== 'string'; 39 | } 40 | 41 | /** 42 | * Configuration for the HttpInterceptor 43 | */ 44 | export interface HttpInterceptorConfig { 45 | allowedList: ApiRouteDefinition[]; 46 | } 47 | 48 | /** 49 | * Configuration for a single interceptor route 50 | */ 51 | export interface HttpInterceptorRouteConfig { 52 | /** 53 | * The URL to test, by supplying the URL to match. 54 | * If `test` is a match for the current request path from the HTTP client, then 55 | * an access token is attached to the request in the 56 | * ["Authorization" header](https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-20#section-2.1). 57 | * 58 | * If the test does not pass, the request proceeds without the access token attached. 59 | * 60 | * A wildcard character can be used to match only the start of the URL. 61 | * 62 | * @usagenotes 63 | * 64 | * '/api' - exactly match the route /api 65 | * '/api/*' - match any route that starts with /api/ 66 | */ 67 | uri?: string; 68 | 69 | /** 70 | * A function that will be called with the HttpRequest.url value, allowing you to do 71 | * any kind of flexible matching. 72 | * 73 | * If this function returns true, then 74 | * an access token is attached to the request in the 75 | * ["Authorization" header](https://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-20#section-2.1). 76 | * 77 | * If it returns false, the request proceeds without the access token attached. 78 | */ 79 | uriMatcher?: (uri: string) => boolean; 80 | 81 | /** 82 | * The options that are passed to the SDK when retrieving the 83 | * access token to attach to the outgoing request. 84 | */ 85 | tokenOptions?: GetTokenSilentlyOptions; 86 | 87 | /** 88 | * The HTTP method to match on. If specified, the HTTP method of 89 | * the outgoing request will be checked against this. If there is no match, the 90 | * Authorization header is not attached. 91 | * 92 | * The HTTP method name is case-sensitive. 93 | */ 94 | httpMethod?: HttpMethod | string; 95 | 96 | /** 97 | * Allow the HTTP call to be executed anonymously, when no token is available. 98 | * 99 | * When omitted (or set to false), calls that match the configuration will fail when no token is available. 100 | */ 101 | allowAnonymous?: boolean; 102 | } 103 | 104 | /** 105 | * Configuration for the authentication service 106 | */ 107 | export interface AuthConfig extends Auth0ClientOptions { 108 | /** 109 | * By default, if the page URL has code and state parameters, the SDK will assume they are for 110 | * an Auth0 application and attempt to exchange the code for a token. 111 | * In some cases the code might be for something else (e.g. another OAuth SDK). In these 112 | * instances you can instruct the client to ignore them by setting `skipRedirectCallback`. 113 | * 114 | * ```js 115 | * AuthModule.forRoot({ 116 | * skipRedirectCallback: window.location.pathname === '/other-callback' 117 | * }) 118 | * ``` 119 | * 120 | * **Note**: In the above example, `/other-callback` is an existing route that will be called 121 | * by any other OAuth provider with a `code` (or `error` in case when something went wrong) and `state`. 122 | * 123 | */ 124 | skipRedirectCallback?: boolean; 125 | 126 | /** 127 | * Configuration for the built-in Http Interceptor, used for 128 | * automatically attaching access tokens. 129 | */ 130 | httpInterceptor?: HttpInterceptorConfig; 131 | 132 | /** 133 | * Path in your application to redirect to when the Authorization server 134 | * returns an error. Defaults to `/` 135 | */ 136 | errorPath?: string; 137 | } 138 | 139 | /** 140 | * Angular specific state to be stored before redirect 141 | */ 142 | export interface AppState { 143 | /** 144 | * Target path the app gets routed to after 145 | * handling the callback from Auth0 (defaults to '/') 146 | */ 147 | target?: string; 148 | 149 | /** 150 | * Any custom parameter to be stored in appState 151 | */ 152 | [key: string]: any; 153 | } 154 | 155 | /** 156 | * Injection token for accessing configuration. 157 | * 158 | * @usageNotes 159 | * 160 | * Use the `Inject` decorator to access the configuration from a service or component: 161 | * 162 | * ``` 163 | * class MyService(@Inject(AuthConfigService) config: AuthConfig) {} 164 | * ``` 165 | */ 166 | export const AuthConfigService = new InjectionToken( 167 | 'auth0-angular.config' 168 | ); 169 | 170 | /** 171 | * Gets and sets configuration for the internal Auth0 client. This can be 172 | * used to provide configuration outside of using AuthModule.forRoot, i.e. from 173 | * a factory provided by APP_INITIALIZER. 174 | * 175 | * @usage 176 | * 177 | * ```js 178 | * // app.module.ts 179 | * // --------------------------- 180 | * import { AuthModule, AuthClientConfig } from '@auth0/auth0-angular'; 181 | * 182 | * // Provide an initializer function that returns a Promise 183 | * function configInitializer( 184 | * http: HttpClient, 185 | * config: AuthClientConfig 186 | * ) { 187 | * return () => 188 | * http 189 | * .get('/config') 190 | * .toPromise() 191 | * .then((loadedConfig: any) => config.set(loadedConfig)); // Set the config that was loaded asynchronously here 192 | * } 193 | * 194 | * // Provide APP_INITIALIZER with this function. Note that there is no config passed to AuthModule.forRoot 195 | * imports: [ 196 | * // other imports.. 197 | * 198 | * HttpClientModule, 199 | * AuthModule.forRoot(), //<- don't pass any config here 200 | * ], 201 | * providers: [ 202 | * { 203 | * provide: APP_INITIALIZER, 204 | * useFactory: configInitializer, // <- pass your initializer function here 205 | * deps: [HttpClient, AuthClientConfig], 206 | * multi: true, 207 | * }, 208 | * ], 209 | * ``` 210 | * 211 | */ 212 | @Injectable({ providedIn: 'root' }) 213 | export class AuthClientConfig { 214 | private config?: AuthConfig; 215 | 216 | constructor(@Optional() @Inject(AuthConfigService) config?: AuthConfig) { 217 | if (config) { 218 | this.set(config); 219 | } 220 | } 221 | 222 | /** 223 | * Sets configuration to be read by other consumers of the service (see usage notes) 224 | * 225 | * @param config The configuration to set 226 | */ 227 | set(config: AuthConfig): void { 228 | this.config = config; 229 | } 230 | 231 | /** 232 | * Gets the config that has been set by other consumers of the service 233 | */ 234 | get(): AuthConfig { 235 | return this.config as AuthConfig; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'rxjs'; 2 | import { AuthGuard } from './auth.guard'; 3 | import { expect } from '@jest/globals'; 4 | 5 | describe('AuthGuard', () => { 6 | let guard: AuthGuard; 7 | const routeMock: any = { snapshot: {} }; 8 | const routeStateMock: any = { snapshot: {}, url: '/' }; 9 | 10 | describe('canActivate', () => { 11 | it('should return true for a logged in user', () => { 12 | const authServiceMock: any = { 13 | isAuthenticated$: of(true), 14 | loginWithRedirect: jest.fn(), 15 | }; 16 | guard = new AuthGuard(authServiceMock); 17 | const listener = jest.fn(); 18 | guard.canActivate(routeMock, routeStateMock).subscribe(listener); 19 | expect(authServiceMock.loginWithRedirect).not.toHaveBeenCalled(); 20 | expect(listener).toHaveBeenCalledWith(true); 21 | }); 22 | 23 | it('should redirect a logged out user', () => { 24 | const authServiceMock: any = { 25 | isAuthenticated$: of(false), 26 | loginWithRedirect: jest.fn(), 27 | }; 28 | guard = new AuthGuard(authServiceMock); 29 | guard.canActivate(routeMock, routeStateMock).subscribe(); 30 | expect(authServiceMock.loginWithRedirect).toHaveBeenCalledWith({ 31 | appState: { target: '/' }, 32 | }); 33 | }); 34 | }); 35 | 36 | describe('canActivateChild', () => { 37 | it('should return true for a logged in user', () => { 38 | const authServiceMock: any = { 39 | isAuthenticated$: of(true), 40 | loginWithRedirect: jest.fn(), 41 | }; 42 | guard = new AuthGuard(authServiceMock); 43 | const listener = jest.fn(); 44 | guard.canActivateChild(routeMock, routeStateMock).subscribe(listener); 45 | expect(authServiceMock.loginWithRedirect).not.toHaveBeenCalled(); 46 | expect(listener).toHaveBeenCalledWith(true); 47 | }); 48 | 49 | it('should redirect a logged out user', () => { 50 | const authServiceMock: any = { 51 | isAuthenticated$: of(false), 52 | loginWithRedirect: jest.fn(), 53 | }; 54 | guard = new AuthGuard(authServiceMock); 55 | guard.canActivateChild(routeMock, routeStateMock).subscribe(); 56 | expect(authServiceMock.loginWithRedirect).toHaveBeenCalledWith({ 57 | appState: { target: '/' }, 58 | }); 59 | }); 60 | }); 61 | 62 | describe('canLoad', () => { 63 | it('should return true for an authenticated user', () => { 64 | const authServiceMock: any = { 65 | isAuthenticated$: of(true), 66 | }; 67 | guard = new AuthGuard(authServiceMock); 68 | const listener = jest.fn(); 69 | guard.canLoad(routeMock, []).subscribe(listener); 70 | expect(listener).toHaveBeenCalledWith(true); 71 | }); 72 | 73 | it('should return false for an unauthenticated user', () => { 74 | const authServiceMock: any = { 75 | isAuthenticated$: of(false), 76 | }; 77 | guard = new AuthGuard(authServiceMock); 78 | const listener = jest.fn(); 79 | guard.canLoad(routeMock, []).subscribe(listener); 80 | expect(listener).toHaveBeenCalledWith(false); 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { 3 | ActivatedRouteSnapshot, 4 | RouterStateSnapshot, 5 | CanActivate, 6 | CanLoad, 7 | Route, 8 | UrlSegment, 9 | CanActivateChild, 10 | } from '@angular/router'; 11 | import { Observable } from 'rxjs'; 12 | import { tap, take } from 'rxjs/operators'; 13 | import { AuthService } from './auth.service'; 14 | 15 | @Injectable({ 16 | providedIn: 'root', 17 | }) 18 | export class AuthGuard implements CanActivate, CanLoad, CanActivateChild { 19 | constructor(private auth: AuthService) {} 20 | 21 | canLoad(route: Route, segments: UrlSegment[]): Observable { 22 | return this.auth.isAuthenticated$.pipe(take(1)); 23 | } 24 | 25 | canActivate( 26 | next: ActivatedRouteSnapshot, 27 | state: RouterStateSnapshot 28 | ): Observable { 29 | return this.redirectIfUnauthenticated(state); 30 | } 31 | 32 | canActivateChild( 33 | childRoute: ActivatedRouteSnapshot, 34 | state: RouterStateSnapshot 35 | ): Observable { 36 | return this.redirectIfUnauthenticated(state); 37 | } 38 | 39 | private redirectIfUnauthenticated( 40 | state: RouterStateSnapshot 41 | ): Observable { 42 | return this.auth.isAuthenticated$.pipe( 43 | tap((loggedIn) => { 44 | if (!loggedIn) { 45 | this.auth.loginWithRedirect({ 46 | appState: { target: state.url }, 47 | }); 48 | } 49 | }) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { AuthHttpInterceptor } from './auth.interceptor'; 2 | import { TestBed, fakeAsync, flush } from '@angular/core/testing'; 3 | import { HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http'; 4 | import { 5 | HttpClientTestingModule, 6 | HttpTestingController, 7 | TestRequest, 8 | } from '@angular/common/http/testing'; 9 | import { Data } from '@angular/router'; 10 | import { 11 | AuthConfig, 12 | HttpMethod, 13 | AuthClientConfig, 14 | HttpInterceptorConfig, 15 | } from './auth.config'; 16 | import { BehaviorSubject, Subject, throwError } from 'rxjs'; 17 | import { Auth0Client } from '@auth0/auth0-spa-js'; 18 | import { Auth0ClientService } from './auth.client'; 19 | import { AuthState } from './auth.state'; 20 | import { AuthService } from './auth.service'; 21 | 22 | // NOTE: Read Async testing: https://github.com/angular/angular/issues/25733#issuecomment-636154553 23 | 24 | const mockWindow = global as any; 25 | 26 | mockWindow.crypto = { 27 | subtle: { 28 | digest: () => 'foo', 29 | }, 30 | getRandomValues() { 31 | return '123'; 32 | }, 33 | }; 34 | 35 | describe('The Auth HTTP Interceptor', () => { 36 | let httpClient: HttpClient; 37 | let httpTestingController: HttpTestingController; 38 | let auth0Client: Auth0Client; 39 | let req: TestRequest; 40 | let authState: AuthState; 41 | const testData: Data = { message: 'Hello, world' }; 42 | let authService: AuthService; 43 | let isLoading$: Subject; 44 | 45 | const assertAuthorizedApiCallTo = async ( 46 | url: string, 47 | done: () => void, 48 | method = 'get' 49 | ) => { 50 | httpClient.request(method, url).subscribe(done); 51 | flush(); 52 | await new Promise(process.nextTick); 53 | req = httpTestingController.expectOne(url); 54 | 55 | expect(req.request.headers.get('Authorization')).toBe( 56 | 'Bearer access-token' 57 | ); 58 | }; 59 | 60 | const assertPassThruApiCallTo = async (url: string, done: () => void) => { 61 | httpClient.get(url).subscribe(done); 62 | flush(); 63 | await new Promise(process.nextTick); 64 | req = httpTestingController.expectOne(url); 65 | expect(req.request.headers.get('Authorization')).toBeFalsy(); 66 | }; 67 | 68 | let config: Partial; 69 | 70 | beforeEach(() => { 71 | isLoading$ = new BehaviorSubject(false); 72 | req = undefined as any; 73 | 74 | auth0Client = new Auth0Client({ 75 | domain: '', 76 | clientId: '', 77 | }); 78 | 79 | jest 80 | .spyOn(auth0Client, 'getTokenSilently') 81 | .mockImplementation(() => Promise.resolve('access-token')); 82 | 83 | config = { 84 | httpInterceptor: { 85 | allowedList: [ 86 | '', 87 | 'https://my-api.com/api/photos', 88 | 'https://my-api.com/api/people*', 89 | 'https://my-api.com/orders', 90 | { 91 | uri: 'https://my-api.com/api/orders', 92 | allowAnonymous: true, 93 | }, 94 | { 95 | uri: 'https://my-api.com/api/addresses', 96 | tokenOptions: { 97 | authorizationParams: { 98 | audience: 'audience', 99 | scope: 'scope', 100 | }, 101 | }, 102 | }, 103 | { 104 | uri: 'https://my-api.com/api/calendar*', 105 | }, 106 | { 107 | uri: 'https://my-api.com/api/register', 108 | httpMethod: HttpMethod.Post, 109 | }, 110 | { 111 | uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1, 112 | httpMethod: HttpMethod.Post, 113 | tokenOptions: { 114 | authorizationParams: { 115 | audience: 'audience', 116 | scope: 'scope', 117 | }, 118 | }, 119 | }, 120 | ], 121 | }, 122 | }; 123 | 124 | TestBed.configureTestingModule({ 125 | imports: [HttpClientTestingModule], 126 | providers: [ 127 | { 128 | provide: HTTP_INTERCEPTORS, 129 | useClass: AuthHttpInterceptor, 130 | multi: true, 131 | }, 132 | { 133 | provide: Auth0ClientService, 134 | useValue: auth0Client, 135 | }, 136 | { 137 | provide: AuthClientConfig, 138 | useValue: { get: () => config }, 139 | }, 140 | { 141 | provide: AuthService, 142 | useValue: { 143 | isLoading$, 144 | }, 145 | }, 146 | ], 147 | }); 148 | 149 | httpClient = TestBed.inject(HttpClient); 150 | httpTestingController = TestBed.inject(HttpTestingController); 151 | authState = TestBed.inject(AuthState); 152 | authService = TestBed.inject(AuthService); 153 | 154 | jest.spyOn(authState, 'setError'); 155 | }); 156 | 157 | afterEach(() => { 158 | httpTestingController.verify(); 159 | if (req) { 160 | req.flush(testData); 161 | } 162 | }); 163 | 164 | describe('When no httpInterceptor is configured', () => { 165 | it('pass through and do not have access tokens attached', fakeAsync(async ( 166 | done: () => void 167 | ) => { 168 | config.httpInterceptor = null as unknown as HttpInterceptorConfig; 169 | await assertPassThruApiCallTo('https://my-api.com/api/public', done); 170 | })); 171 | }); 172 | 173 | describe('Requests that do not require authentication', () => { 174 | it('pass through and do not have access tokens attached', fakeAsync(async ( 175 | done: () => void 176 | ) => { 177 | await assertPassThruApiCallTo('https://my-api.com/api/public', done); 178 | })); 179 | }); 180 | 181 | describe('Requests that are configured using a primitive', () => { 182 | it('waits unil isLoading emits false', fakeAsync(async ( 183 | done: () => void 184 | ) => { 185 | const method = 'GET'; 186 | const url = 'https://my-api.com/api/photos'; 187 | 188 | isLoading$.next(true); 189 | 190 | httpClient.request(method, url).subscribe(done); 191 | flush(); 192 | 193 | httpTestingController.expectNone(url); 194 | 195 | isLoading$.next(false); 196 | flush(); 197 | 198 | httpTestingController.expectOne(url); 199 | })); 200 | 201 | it('attach the access token when the configuration uri is a string', fakeAsync(async ( 202 | done: () => void 203 | ) => { 204 | // Testing /api/photos (exact match) 205 | await assertAuthorizedApiCallTo('https://my-api.com/api/photos', done); 206 | })); 207 | 208 | it('attach the access token when the configuration uri is a string with a wildcard', fakeAsync(async ( 209 | done: () => void 210 | ) => { 211 | // Testing /api/people* (wildcard match) 212 | await assertAuthorizedApiCallTo( 213 | 'https://my-api.com/api/people/profile', 214 | done 215 | ); 216 | })); 217 | 218 | it('matches a full url to an API', fakeAsync(async (done: () => void) => { 219 | // Testing 'https://my-api.com/orders' (exact) 220 | await assertAuthorizedApiCallTo('https://my-api.com/orders', done); 221 | })); 222 | 223 | it('matches a URL that contains a query string', fakeAsync(async ( 224 | done: () => void 225 | ) => { 226 | await assertAuthorizedApiCallTo( 227 | 'https://my-api.com/api/people?name=test', 228 | done 229 | ); 230 | })); 231 | 232 | it('matches a URL that contains a hash fragment', fakeAsync(async ( 233 | done: () => void 234 | ) => { 235 | await assertAuthorizedApiCallTo( 236 | 'https://my-api.com/api/people#hash-fragment', 237 | done 238 | ); 239 | })); 240 | }); 241 | 242 | describe('Requests that are configured using a complex object', () => { 243 | it('waits unil isLoading emits false', fakeAsync(async ( 244 | done: () => void 245 | ) => { 246 | const method = 'GET'; 247 | const url = 'https://my-api.com/api/orders'; 248 | 249 | isLoading$.next(true); 250 | 251 | httpClient.request(method, url).subscribe(done); 252 | flush(); 253 | 254 | httpTestingController.expectNone(url); 255 | 256 | isLoading$.next(false); 257 | flush(); 258 | 259 | httpTestingController.expectOne(url); 260 | })); 261 | 262 | it('attach the access token when the uri is configured using a string', fakeAsync(async ( 263 | done: () => void 264 | ) => { 265 | // Testing { uri: /api/orders } (exact match) 266 | await assertAuthorizedApiCallTo('https://my-api.com/api/orders', done); 267 | })); 268 | 269 | it('pass through the route options to getTokenSilently, without additional properties', fakeAsync(async ( 270 | done: () => void 271 | ) => { 272 | // Testing { uri: /api/addresses } (exact match) 273 | await assertAuthorizedApiCallTo('https://my-api.com/api/addresses', done); 274 | 275 | expect(auth0Client.getTokenSilently).toHaveBeenCalledWith({ 276 | authorizationParams: { 277 | audience: 'audience', 278 | scope: 'scope', 279 | }, 280 | }); 281 | })); 282 | 283 | it('attach the access token when the configuration uri is a string with a wildcard', fakeAsync(async ( 284 | done: () => void 285 | ) => { 286 | // Testing { uri: /api/calendar* } (wildcard match) 287 | await assertAuthorizedApiCallTo( 288 | 'https://my-api.com/api/calendar/events', 289 | done 290 | ); 291 | })); 292 | 293 | it('attaches the access token when the HTTP method matches', fakeAsync(async ( 294 | done: () => void 295 | ) => { 296 | // Testing { uri: /api/register } (wildcard match) 297 | await assertAuthorizedApiCallTo( 298 | 'https://my-api.com/api/register', 299 | done, 300 | 'post' 301 | ); 302 | })); 303 | 304 | it('does not attach the access token if the HTTP method does not match', fakeAsync(async ( 305 | done: () => void 306 | ) => { 307 | await assertPassThruApiCallTo('https://my-api.com/api/public', done); 308 | })); 309 | 310 | it('does not execute HTTP call when not able to retrieve a token', fakeAsync(async ( 311 | done: () => void 312 | ) => { 313 | ( 314 | auth0Client.getTokenSilently as unknown as jest.SpyInstance 315 | ).mockReturnValue(throwError({ error: 'login_required' })); 316 | 317 | httpClient.request('get', 'https://my-api.com/api/calendar').subscribe({ 318 | error: (err) => expect(err).toEqual({ error: 'login_required' }), 319 | }); 320 | 321 | flush(); 322 | await new Promise(process.nextTick); 323 | 324 | httpTestingController.expectNone('https://my-api.com/api/calendar'); 325 | })); 326 | 327 | it('does execute HTTP call when not able to retrieve a token but allowAnonymous is set to true', fakeAsync(async ( 328 | done: () => void 329 | ) => { 330 | ( 331 | auth0Client.getTokenSilently as unknown as jest.SpyInstance 332 | ).mockReturnValue(throwError({ error: 'login_required' })); 333 | 334 | await assertPassThruApiCallTo('https://my-api.com/api/orders', done); 335 | })); 336 | 337 | it('does execute HTTP call when missing_refresh_token but allowAnonymous is set to true', fakeAsync(async ( 338 | done: () => void 339 | ) => { 340 | ( 341 | auth0Client.getTokenSilently as unknown as jest.SpyInstance 342 | ).mockReturnValue(throwError({ error: 'missing_refresh_token' })); 343 | 344 | await assertPassThruApiCallTo('https://my-api.com/api/orders', done); 345 | })); 346 | 347 | it('emit error when not able to retrieve a token but allowAnonymous is set to false', fakeAsync(async ( 348 | done: () => void 349 | ) => { 350 | ( 351 | auth0Client.getTokenSilently as unknown as jest.SpyInstance 352 | ).mockRejectedValue({ error: 'login_required' }); 353 | 354 | httpClient.request('get', 'https://my-api.com/api/calendar').subscribe({ 355 | error: (err) => expect(err).toEqual({ error: 'login_required' }), 356 | }); 357 | 358 | flush(); 359 | await new Promise(process.nextTick); 360 | 361 | httpTestingController.expectNone('https://my-api.com/api/calendar'); 362 | 363 | expect(authState.setError).toHaveBeenCalled(); 364 | })); 365 | 366 | it('does not emit error when not able to retrieve a token but allowAnonymous is set to true', fakeAsync(async () => { 367 | ( 368 | auth0Client.getTokenSilently as unknown as jest.SpyInstance 369 | ).mockRejectedValue({ error: 'login_required' }); 370 | 371 | await assertPassThruApiCallTo('https://my-api.com/api/orders', () => { 372 | expect(authState.setError).not.toHaveBeenCalled(); 373 | }); 374 | })); 375 | 376 | it('does not emit error when missing_refresh_token but allowAnonymous is set to true', fakeAsync(async () => { 377 | ( 378 | auth0Client.getTokenSilently as unknown as jest.SpyInstance 379 | ).mockRejectedValue({ error: 'missing_refresh_token' }); 380 | 381 | await assertPassThruApiCallTo('https://my-api.com/api/orders', () => { 382 | expect(authState.setError).not.toHaveBeenCalled(); 383 | }); 384 | })); 385 | }); 386 | 387 | describe('Requests that are configured using an uri matcher', () => { 388 | it('waits unil isLoading emits false', fakeAsync(async ( 389 | done: () => void 390 | ) => { 391 | const method = 'GET'; 392 | const url = 'https://my-api.com/api/orders'; 393 | 394 | isLoading$.next(true); 395 | 396 | httpClient.request(method, url).subscribe(done); 397 | flush(); 398 | 399 | httpTestingController.expectNone(url); 400 | 401 | isLoading$.next(false); 402 | flush(); 403 | 404 | httpTestingController.expectOne(url); 405 | })); 406 | 407 | it('attach the access token when the matcher returns true', fakeAsync(async ( 408 | done: () => void 409 | ) => { 410 | // Testing { uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1 } 411 | await assertAuthorizedApiCallTo( 412 | 'https://my-api.com/api/contact', 413 | done, 414 | 'post' 415 | ); 416 | })); 417 | 418 | it('pass through the route options to getTokenSilently, without additional properties', fakeAsync(async ( 419 | done: () => void 420 | ) => { 421 | // Testing { uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1 } 422 | await assertAuthorizedApiCallTo( 423 | 'https://my-api.com/api/contact', 424 | done, 425 | 'post' 426 | ); 427 | 428 | expect(auth0Client.getTokenSilently).toHaveBeenCalledWith({ 429 | authorizationParams: { 430 | audience: 'audience', 431 | scope: 'scope', 432 | }, 433 | }); 434 | })); 435 | 436 | it('does attach the access token when the HTTP method does match', fakeAsync(async ( 437 | done: () => void 438 | ) => { 439 | // Testing { uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1 } 440 | await assertAuthorizedApiCallTo( 441 | 'https://my-api.com/api/contact', 442 | done, 443 | 'post' 444 | ); 445 | })); 446 | 447 | it('does not attach the access token when the HTTP method does not match', fakeAsync(async ( 448 | done: () => void 449 | ) => { 450 | // Testing { uriMatcher: (uri) => uri.indexOf('/api/contact') !== -1 } 451 | await assertPassThruApiCallTo('https://my-api.com/api/contact', done); 452 | })); 453 | }); 454 | }); 455 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpInterceptor, 3 | HttpRequest, 4 | HttpHandler, 5 | HttpEvent, 6 | } from '@angular/common/http'; 7 | 8 | import { Observable, from, of, iif, throwError } from 'rxjs'; 9 | import { Inject, Injectable } from '@angular/core'; 10 | 11 | import { 12 | ApiRouteDefinition, 13 | isHttpInterceptorRouteConfig, 14 | AuthClientConfig, 15 | HttpInterceptorConfig, 16 | } from './auth.config'; 17 | 18 | import { 19 | switchMap, 20 | first, 21 | concatMap, 22 | catchError, 23 | tap, 24 | filter, 25 | mergeMap, 26 | mapTo, 27 | pluck, 28 | } from 'rxjs/operators'; 29 | import { Auth0Client, GetTokenSilentlyOptions } from '@auth0/auth0-spa-js'; 30 | import { Auth0ClientService } from './auth.client'; 31 | import { AuthState } from './auth.state'; 32 | import { AuthService } from './auth.service'; 33 | 34 | const waitUntil = 35 | (signal$: Observable) => 36 | (source$: Observable) => 37 | source$.pipe(mergeMap((value) => signal$.pipe(first(), mapTo(value)))); 38 | 39 | @Injectable() 40 | export class AuthHttpInterceptor implements HttpInterceptor { 41 | constructor( 42 | private configFactory: AuthClientConfig, 43 | @Inject(Auth0ClientService) private auth0Client: Auth0Client, 44 | private authState: AuthState, 45 | private authService: AuthService 46 | ) {} 47 | 48 | intercept( 49 | req: HttpRequest, 50 | next: HttpHandler 51 | ): Observable> { 52 | const config = this.configFactory.get(); 53 | if (!config.httpInterceptor?.allowedList) { 54 | return next.handle(req); 55 | } 56 | 57 | const isLoaded$ = this.authService.isLoading$.pipe( 58 | filter((isLoading) => !isLoading) 59 | ); 60 | 61 | return this.findMatchingRoute(req, config.httpInterceptor).pipe( 62 | concatMap((route) => 63 | iif( 64 | // Check if a route was matched 65 | () => route !== null, 66 | // If we have a matching route, call getTokenSilently and attach the token to the 67 | // outgoing request 68 | of(route).pipe( 69 | waitUntil(isLoaded$), 70 | pluck('tokenOptions'), 71 | concatMap>((options) => 72 | this.getAccessTokenSilently(options).pipe( 73 | catchError((err) => { 74 | if (this.allowAnonymous(route, err)) { 75 | return of(''); 76 | } 77 | 78 | this.authState.setError(err); 79 | return throwError(err); 80 | }) 81 | ) 82 | ), 83 | switchMap((token: string) => { 84 | // Clone the request and attach the bearer token 85 | const clone = token 86 | ? req.clone({ 87 | headers: req.headers.set( 88 | 'Authorization', 89 | `Bearer ${token}` 90 | ), 91 | }) 92 | : req; 93 | 94 | return next.handle(clone); 95 | }) 96 | ), 97 | // If the URI being called was not found in our httpInterceptor config, simply 98 | // pass the request through without attaching a token 99 | next.handle(req) 100 | ) 101 | ) 102 | ); 103 | } 104 | 105 | /** 106 | * Duplicate of AuthService.getAccessTokenSilently, but with a slightly different error handling. 107 | * Only used internally in the interceptor. 108 | * 109 | * @param options The options for configuring the token fetch. 110 | */ 111 | private getAccessTokenSilently( 112 | options?: GetTokenSilentlyOptions 113 | ): Observable { 114 | return of(this.auth0Client).pipe( 115 | concatMap((client) => client.getTokenSilently(options)), 116 | tap((token) => this.authState.setAccessToken(token)), 117 | catchError((error) => { 118 | this.authState.refresh(); 119 | return throwError(error); 120 | }) 121 | ); 122 | } 123 | 124 | /** 125 | * Strips the query and fragment from the given uri 126 | * 127 | * @param uri The uri to remove the query and fragment from 128 | */ 129 | private stripQueryFrom(uri: string): string { 130 | if (uri.indexOf('?') > -1) { 131 | uri = uri.substr(0, uri.indexOf('?')); 132 | } 133 | 134 | if (uri.indexOf('#') > -1) { 135 | uri = uri.substr(0, uri.indexOf('#')); 136 | } 137 | 138 | return uri; 139 | } 140 | 141 | /** 142 | * Determines whether the specified route can have an access token attached to it, based on matching the HTTP request against 143 | * the interceptor route configuration. 144 | * 145 | * @param route The route to test 146 | * @param request The HTTP request 147 | */ 148 | private canAttachToken( 149 | route: ApiRouteDefinition, 150 | request: HttpRequest 151 | ): boolean { 152 | const testPrimitive = (value: string | undefined): boolean => { 153 | if (!value) { 154 | return false; 155 | } 156 | 157 | const requestPath = this.stripQueryFrom(request.url); 158 | 159 | if (value === requestPath) { 160 | return true; 161 | } 162 | 163 | // If the URL ends with an asterisk, match using startsWith. 164 | return ( 165 | value.indexOf('*') === value.length - 1 && 166 | request.url.startsWith(value.substr(0, value.length - 1)) 167 | ); 168 | }; 169 | 170 | if (isHttpInterceptorRouteConfig(route)) { 171 | if (route.httpMethod && route.httpMethod !== request.method) { 172 | return false; 173 | } 174 | 175 | /* istanbul ignore if */ 176 | if (!route.uri && !route.uriMatcher) { 177 | console.warn( 178 | 'Either a uri or uriMatcher is required when configuring the HTTP interceptor.' 179 | ); 180 | } 181 | 182 | return route.uriMatcher 183 | ? route.uriMatcher(request.url) 184 | : testPrimitive(route.uri); 185 | } 186 | 187 | return testPrimitive(route); 188 | } 189 | 190 | /** 191 | * Tries to match a route from the SDK configuration to the HTTP request. 192 | * If a match is found, the route configuration is returned. 193 | * 194 | * @param request The Http request 195 | * @param config HttpInterceptorConfig 196 | */ 197 | private findMatchingRoute( 198 | request: HttpRequest, 199 | config: HttpInterceptorConfig 200 | ): Observable { 201 | return from(config.allowedList).pipe( 202 | first((route) => this.canAttachToken(route, request), null) 203 | ); 204 | } 205 | 206 | private allowAnonymous(route: ApiRouteDefinition | null, err: any): boolean { 207 | return ( 208 | !!route && 209 | isHttpInterceptorRouteConfig(route) && 210 | !!route.allowAnonymous && 211 | ['login_required', 'consent_required', 'missing_refresh_token'].includes( 212 | err.error 213 | ) 214 | ); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ModuleWithProviders } from '@angular/core'; 2 | import { AuthService } from './auth.service'; 3 | import { AuthConfig, AuthConfigService, AuthClientConfig } from './auth.config'; 4 | import { Auth0ClientService, Auth0ClientFactory } from './auth.client'; 5 | import { AuthGuard } from './auth.guard'; 6 | 7 | @NgModule() 8 | export class AuthModule { 9 | /** 10 | * Initialize the authentication module system. Configuration can either be specified here, 11 | * or by calling AuthClientConfig.set (perhaps from an APP_INITIALIZER factory function). 12 | * 13 | * @param config The optional configuration for the SDK. 14 | */ 15 | static forRoot(config?: AuthConfig): ModuleWithProviders { 16 | return { 17 | ngModule: AuthModule, 18 | providers: [ 19 | AuthService, 20 | AuthGuard, 21 | { 22 | provide: AuthConfigService, 23 | useValue: config, 24 | }, 25 | { 26 | provide: Auth0ClientService, 27 | useFactory: Auth0ClientFactory.createClient, 28 | deps: [AuthClientConfig], 29 | }, 30 | ], 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Inject, OnDestroy } from '@angular/core'; 2 | 3 | import { 4 | Auth0Client, 5 | PopupLoginOptions, 6 | PopupConfigOptions, 7 | GetTokenSilentlyOptions, 8 | GetTokenWithPopupOptions, 9 | RedirectLoginResult, 10 | GetTokenSilentlyVerboseResponse, 11 | } from '@auth0/auth0-spa-js'; 12 | 13 | import { 14 | of, 15 | from, 16 | Subject, 17 | Observable, 18 | iif, 19 | defer, 20 | ReplaySubject, 21 | throwError, 22 | } from 'rxjs'; 23 | 24 | import { 25 | concatMap, 26 | tap, 27 | map, 28 | takeUntil, 29 | catchError, 30 | switchMap, 31 | withLatestFrom, 32 | } from 'rxjs/operators'; 33 | 34 | import { Auth0ClientService } from './auth.client'; 35 | import { AbstractNavigator } from './abstract-navigator'; 36 | import { AuthClientConfig, AppState } from './auth.config'; 37 | import { AuthState } from './auth.state'; 38 | import { LogoutOptions, RedirectLoginOptions } from './interfaces'; 39 | 40 | @Injectable({ 41 | providedIn: 'root', 42 | }) 43 | export class AuthService 44 | implements OnDestroy 45 | { 46 | private appStateSubject$ = new ReplaySubject(1); 47 | 48 | // https://stackoverflow.com/a/41177163 49 | private ngUnsubscribe$ = new Subject(); 50 | /** 51 | * Emits boolean values indicating the loading state of the SDK. 52 | */ 53 | readonly isLoading$ = this.authState.isLoading$; 54 | 55 | /** 56 | * Emits boolean values indicating the authentication state of the user. If `true`, it means a user has authenticated. 57 | * This depends on the value of `isLoading$`, so there is no need to manually check the loading state of the SDK. 58 | */ 59 | readonly isAuthenticated$ = this.authState.isAuthenticated$; 60 | 61 | /** 62 | * Emits details about the authenticated user, or null if not authenticated. 63 | */ 64 | readonly user$ = this.authState.user$; 65 | 66 | /** 67 | * Emits ID token claims when authenticated, or null if not authenticated. 68 | */ 69 | readonly idTokenClaims$ = this.authState.idTokenClaims$; 70 | 71 | /** 72 | * Emits errors that occur during login, or when checking for an active session on startup. 73 | */ 74 | readonly error$ = this.authState.error$; 75 | 76 | /** 77 | * Emits the value (if any) that was passed to the `loginWithRedirect` method call 78 | * but only **after** `handleRedirectCallback` is first called 79 | */ 80 | readonly appState$ = this.appStateSubject$.asObservable(); 81 | 82 | constructor( 83 | @Inject(Auth0ClientService) private auth0Client: Auth0Client, 84 | private configFactory: AuthClientConfig, 85 | private navigator: AbstractNavigator, 86 | private authState: AuthState 87 | ) { 88 | const checkSessionOrCallback$ = (isCallback: boolean) => 89 | iif( 90 | () => isCallback, 91 | this.handleRedirectCallback(), 92 | defer(() => this.auth0Client.checkSession()) 93 | ); 94 | 95 | this.shouldHandleCallback() 96 | .pipe( 97 | switchMap((isCallback) => 98 | checkSessionOrCallback$(isCallback).pipe( 99 | catchError((error) => { 100 | const config = this.configFactory.get(); 101 | this.navigator.navigateByUrl(config.errorPath || '/'); 102 | this.authState.setError(error); 103 | return of(undefined); 104 | }) 105 | ) 106 | ), 107 | tap(() => { 108 | this.authState.setIsLoading(false); 109 | }), 110 | takeUntil(this.ngUnsubscribe$) 111 | ) 112 | .subscribe(); 113 | } 114 | 115 | /** 116 | * Called when the service is destroyed 117 | */ 118 | ngOnDestroy(): void { 119 | // https://stackoverflow.com/a/41177163 120 | this.ngUnsubscribe$.next(); 121 | this.ngUnsubscribe$.complete(); 122 | } 123 | 124 | /** 125 | * ```js 126 | * loginWithRedirect(options); 127 | * ``` 128 | * 129 | * Performs a redirect to `/authorize` using the parameters 130 | * provided as arguments. Random and secure `state` and `nonce` 131 | * parameters will be auto-generated. 132 | * 133 | * @param options The login options 134 | */ 135 | loginWithRedirect( 136 | options?: RedirectLoginOptions 137 | ): Observable { 138 | return from(this.auth0Client.loginWithRedirect(options)); 139 | } 140 | 141 | /** 142 | * ```js 143 | * await loginWithPopup(options); 144 | * ``` 145 | * 146 | * Opens a popup with the `/authorize` URL using the parameters 147 | * provided as arguments. Random and secure `state` and `nonce` 148 | * parameters will be auto-generated. If the response is successful, 149 | * results will be valid according to their expiration times. 150 | * 151 | * IMPORTANT: This method has to be called from an event handler 152 | * that was started by the user like a button click, for example, 153 | * otherwise the popup will be blocked in most browsers. 154 | * 155 | * @param options The login options 156 | * @param config Configuration for the popup window 157 | */ 158 | loginWithPopup( 159 | options?: PopupLoginOptions, 160 | config?: PopupConfigOptions 161 | ): Observable { 162 | return from( 163 | this.auth0Client.loginWithPopup(options, config).then(() => { 164 | this.authState.refresh(); 165 | }) 166 | ); 167 | } 168 | 169 | /** 170 | * ```js 171 | * logout(); 172 | * ``` 173 | * 174 | * Clears the application session and performs a redirect to `/v2/logout`, using 175 | * the parameters provided as arguments, to clear the Auth0 session. 176 | * If the `federated` option is specified it also clears the Identity Provider session. 177 | * If the `openUrl` option is set to false, it only clears the application session. 178 | * It is invalid to set both the `federated` to true and `openUrl` to `false`, 179 | * and an error will be thrown if you do. 180 | * [Read more about how Logout works at Auth0](https://auth0.com/docs/logout). 181 | * 182 | * @param options The logout options 183 | */ 184 | logout(options?: LogoutOptions): Observable { 185 | return from( 186 | this.auth0Client.logout(options).then(() => { 187 | if (options?.openUrl === false || options?.openUrl) { 188 | this.authState.refresh(); 189 | } 190 | }) 191 | ); 192 | } 193 | 194 | /** 195 | * Fetches a new access token and returns the response from the /oauth/token endpoint, omitting the refresh token. 196 | * 197 | * @param options The options for configuring the token fetch. 198 | */ 199 | getAccessTokenSilently( 200 | options: GetTokenSilentlyOptions & { detailedResponse: true } 201 | ): Observable; 202 | 203 | /** 204 | * Fetches a new access token and returns it. 205 | * 206 | * @param options The options for configuring the token fetch. 207 | */ 208 | getAccessTokenSilently(options?: GetTokenSilentlyOptions): Observable; 209 | 210 | /** 211 | * ```js 212 | * getAccessTokenSilently(options).subscribe(token => ...) 213 | * ``` 214 | * 215 | * If there's a valid token stored, return it. Otherwise, opens an 216 | * iframe with the `/authorize` URL using the parameters provided 217 | * as arguments. Random and secure `state` and `nonce` parameters 218 | * will be auto-generated. If the response is successful, results 219 | * will be valid according to their expiration times. 220 | * 221 | * If refresh tokens are used, the token endpoint is called directly with the 222 | * 'refresh_token' grant. If no refresh token is available to make this call, 223 | * the SDK falls back to using an iframe to the '/authorize' URL. 224 | * 225 | * This method may use a web worker to perform the token call if the in-memory 226 | * cache is used. 227 | * 228 | * If an `audience` value is given to this function, the SDK always falls 229 | * back to using an iframe to make the token exchange. 230 | * 231 | * Note that in all cases, falling back to an iframe requires access to 232 | * the `auth0` cookie, and thus will not work in browsers that block third-party 233 | * cookies by default (Safari, Brave, etc). 234 | * 235 | * @param options The options for configuring the token fetch. 236 | */ 237 | getAccessTokenSilently( 238 | options: GetTokenSilentlyOptions = {} 239 | ): Observable { 240 | return of(this.auth0Client).pipe( 241 | concatMap((client) => 242 | options.detailedResponse === true 243 | ? client.getTokenSilently({ ...options, detailedResponse: true }) 244 | : client.getTokenSilently(options) 245 | ), 246 | tap((token) => { 247 | if (token) { 248 | this.authState.setAccessToken( 249 | typeof token === 'string' ? token : token.access_token 250 | ); 251 | } 252 | }), 253 | catchError((error) => { 254 | this.authState.setError(error); 255 | this.authState.refresh(); 256 | return throwError(error); 257 | }) 258 | ); 259 | } 260 | 261 | /** 262 | * ```js 263 | * getTokenWithPopup(options).subscribe(token => ...) 264 | * ``` 265 | * 266 | * Get an access token interactively. 267 | * 268 | * Opens a popup with the `/authorize` URL using the parameters 269 | * provided as arguments. Random and secure `state` and `nonce` 270 | * parameters will be auto-generated. If the response is successful, 271 | * results will be valid according to their expiration times. 272 | */ 273 | getAccessTokenWithPopup( 274 | options?: GetTokenWithPopupOptions 275 | ): Observable { 276 | return of(this.auth0Client).pipe( 277 | concatMap((client) => client.getTokenWithPopup(options)), 278 | tap((token) => { 279 | if (token) { 280 | this.authState.setAccessToken(token); 281 | } 282 | }), 283 | catchError((error) => { 284 | this.authState.setError(error); 285 | this.authState.refresh(); 286 | return throwError(error); 287 | }) 288 | ); 289 | } 290 | 291 | /** 292 | * ```js 293 | * handleRedirectCallback(url).subscribe(result => ...) 294 | * ``` 295 | * 296 | * After the browser redirects back to the callback page, 297 | * call `handleRedirectCallback` to handle success and error 298 | * responses from Auth0. If the response is successful, results 299 | * will be valid according to their expiration times. 300 | * 301 | * Calling this method also refreshes the authentication and user states. 302 | * 303 | * @param url The URL to that should be used to retrieve the `state` and `code` values. Defaults to `window.location.href` if not given. 304 | */ 305 | handleRedirectCallback( 306 | url?: string 307 | ): Observable> { 308 | return defer(() => 309 | this.auth0Client.handleRedirectCallback(url) 310 | ).pipe( 311 | withLatestFrom(this.authState.isLoading$), 312 | tap(([result, isLoading]) => { 313 | if (!isLoading) { 314 | this.authState.refresh(); 315 | } 316 | const appState = result?.appState; 317 | const target = appState?.target ?? '/'; 318 | 319 | if (appState) { 320 | this.appStateSubject$.next(appState); 321 | } 322 | 323 | this.navigator.navigateByUrl(target); 324 | }), 325 | map(([result]) => result) 326 | ); 327 | } 328 | 329 | private shouldHandleCallback(): Observable { 330 | return of(location.search).pipe( 331 | map((search) => { 332 | const searchParams = new URLSearchParams(search); 333 | return ( 334 | (searchParams.has('code') || searchParams.has('error')) && 335 | searchParams.has('state') && 336 | !this.configFactory.get().skipRedirectCallback 337 | ); 338 | }) 339 | ); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/auth.state.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@angular/core'; 2 | import { Auth0Client } from '@auth0/auth0-spa-js'; 3 | import { 4 | BehaviorSubject, 5 | defer, 6 | merge, 7 | of, 8 | ReplaySubject, 9 | Subject, 10 | } from 'rxjs'; 11 | import { 12 | concatMap, 13 | distinctUntilChanged, 14 | filter, 15 | mergeMap, 16 | scan, 17 | shareReplay, 18 | switchMap, 19 | } from 'rxjs/operators'; 20 | import { Auth0ClientService } from './auth.client'; 21 | 22 | /** 23 | * Tracks the Authentication State for the SDK 24 | */ 25 | @Injectable({ providedIn: 'root' }) 26 | export class AuthState { 27 | private isLoadingSubject$ = new BehaviorSubject(true); 28 | private refresh$ = new Subject(); 29 | private accessToken$ = new ReplaySubject(1); 30 | private errorSubject$ = new ReplaySubject(1); 31 | 32 | /** 33 | * Emits boolean values indicating the loading state of the SDK. 34 | */ 35 | public readonly isLoading$ = this.isLoadingSubject$.asObservable(); 36 | 37 | /** 38 | * Trigger used to pull User information from the Auth0Client. 39 | * Triggers when the access token has changed. 40 | */ 41 | private accessTokenTrigger$ = this.accessToken$.pipe( 42 | scan( 43 | ( 44 | acc: { current: string | null; previous: string | null }, 45 | current: string | null 46 | ) => ({ 47 | previous: acc.current, 48 | current, 49 | }), 50 | { current: null, previous: null } 51 | ), 52 | filter(({ previous, current }) => previous !== current) 53 | ); 54 | 55 | /** 56 | * Trigger used to pull User information from the Auth0Client. 57 | * Triggers when an event occurs that needs to retrigger the User Profile information. 58 | * Events: Login, Access Token change and Logout 59 | */ 60 | private readonly isAuthenticatedTrigger$ = this.isLoading$.pipe( 61 | filter((loading) => !loading), 62 | distinctUntilChanged(), 63 | switchMap(() => 64 | // To track the value of isAuthenticated over time, we need to merge: 65 | // - the current value 66 | // - the value whenever the access token changes. (this should always be true of there is an access token 67 | // but it is safer to pass this through this.auth0Client.isAuthenticated() nevertheless) 68 | // - the value whenever refreshState$ emits 69 | merge( 70 | defer(() => this.auth0Client.isAuthenticated()), 71 | this.accessTokenTrigger$.pipe( 72 | mergeMap(() => this.auth0Client.isAuthenticated()) 73 | ), 74 | this.refresh$.pipe(mergeMap(() => this.auth0Client.isAuthenticated())) 75 | ) 76 | ) 77 | ); 78 | 79 | /** 80 | * Emits boolean values indicating the authentication state of the user. If `true`, it means a user has authenticated. 81 | * This depends on the value of `isLoading$`, so there is no need to manually check the loading state of the SDK. 82 | */ 83 | readonly isAuthenticated$ = this.isAuthenticatedTrigger$.pipe( 84 | distinctUntilChanged(), 85 | shareReplay(1) 86 | ); 87 | 88 | /** 89 | * Emits details about the authenticated user, or null if not authenticated. 90 | */ 91 | readonly user$ = this.isAuthenticatedTrigger$.pipe( 92 | concatMap((authenticated) => 93 | authenticated ? this.auth0Client.getUser() : of(null) 94 | ), 95 | distinctUntilChanged() 96 | ); 97 | 98 | /** 99 | * Emits ID token claims when authenticated, or null if not authenticated. 100 | */ 101 | readonly idTokenClaims$ = this.isAuthenticatedTrigger$.pipe( 102 | concatMap((authenticated) => 103 | authenticated ? this.auth0Client.getIdTokenClaims() : of(null) 104 | ) 105 | ); 106 | 107 | /** 108 | * Emits errors that occur during login, or when checking for an active session on startup. 109 | */ 110 | public readonly error$ = this.errorSubject$.asObservable(); 111 | 112 | constructor(@Inject(Auth0ClientService) private auth0Client: Auth0Client) {} 113 | 114 | /** 115 | * Update the isLoading state using the provided value 116 | * 117 | * @param isLoading The new value for isLoading 118 | */ 119 | public setIsLoading(isLoading: boolean): void { 120 | this.isLoadingSubject$.next(isLoading); 121 | } 122 | 123 | /** 124 | * Refresh the state to ensure the `isAuthenticated`, `user$` and `idTokenClaims$` 125 | * reflect the most up-to-date values from Auth0Client. 126 | */ 127 | public refresh(): void { 128 | this.refresh$.next(); 129 | } 130 | 131 | /** 132 | * Update the access token, doing so will also refresh the state. 133 | * 134 | * @param accessToken The new Access Token 135 | */ 136 | public setAccessToken(accessToken: string): void { 137 | this.accessToken$.next(accessToken); 138 | } 139 | 140 | /** 141 | * Emits the error in the `error$` observable. 142 | * 143 | * @param error The new error 144 | */ 145 | public setError(error: any): void { 146 | this.errorSubject$.next(error); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/functional.ts: -------------------------------------------------------------------------------- 1 | import { HttpEvent, HttpRequest } from '@angular/common/http'; 2 | import { inject } from '@angular/core'; 3 | import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 4 | import { Observable } from 'rxjs'; 5 | import { AuthGuard } from './auth.guard'; 6 | import { AuthHttpInterceptor } from './auth.interceptor'; 7 | 8 | /** 9 | * Functional AuthGuard to ensure routes can only be accessed when authenticated. 10 | * 11 | * Note: Should only be used as of Angular 15 12 | * 13 | * @param route Contains the information about a route associated with a component loaded in an outlet at a particular moment in time. 14 | * @param state Represents the state of the router at a moment in time. 15 | * @returns An Observable, indicating if the route can be accessed or not 16 | */ 17 | export const authGuardFn = ( 18 | route: ActivatedRouteSnapshot, 19 | state: RouterStateSnapshot 20 | ) => inject(AuthGuard).canActivate(route, state); 21 | 22 | /** 23 | * Functional AuthHttpInterceptor to include the access token in matching requests. 24 | * 25 | * Note: Should only be used as of Angular 15 26 | * 27 | * @param req An outgoing HTTP request with an optional typed body. 28 | * @param handle Represents the next interceptor in an interceptor chain, or the real backend if there are no 29 | * further interceptors. 30 | * @returns An Observable representing the intercepted HttpRequest 31 | */ 32 | export const authHttpInterceptorFn = ( 33 | req: HttpRequest, 34 | handle: (req: HttpRequest) => Observable> 35 | ) => inject(AuthHttpInterceptor).intercept(req, { handle }); 36 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { RedirectLoginOptions as SPARedirectLoginOptions, LogoutOptions as SPALogoutOptions } from '@auth0/auth0-spa-js'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 4 | export interface RedirectLoginOptions extends Omit, 'onRedirect'> {} 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 7 | export interface LogoutOptions extends Omit {} 8 | 9 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/lib/provide.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@angular/core'; 2 | import { Auth0ClientService, Auth0ClientFactory } from './auth.client'; 3 | import { AuthConfig, AuthConfigService, AuthClientConfig } from './auth.config'; 4 | import { AuthGuard } from './auth.guard'; 5 | import { AuthHttpInterceptor } from './auth.interceptor'; 6 | import { AuthService } from './auth.service'; 7 | 8 | /** 9 | * Initialize the authentication system. Configuration can either be specified here, 10 | * or by calling AuthClientConfig.set (perhaps from an APP_INITIALIZER factory function). 11 | * 12 | * Note: Should only be used as of Angular 15, and should not be added to a component's providers. 13 | * 14 | * @param config The optional configuration for the SDK. 15 | * 16 | * @example 17 | * bootstrapApplication(AppComponent, { 18 | * providers: [ 19 | * provideAuth0(), 20 | * ], 21 | * }); 22 | */ 23 | export function provideAuth0(config?: AuthConfig): Provider[] { 24 | return [ 25 | AuthService, 26 | AuthHttpInterceptor, 27 | AuthGuard, 28 | { 29 | provide: AuthConfigService, 30 | useValue: config, 31 | }, 32 | { 33 | provide: Auth0ClientService, 34 | useFactory: Auth0ClientFactory.createClient, 35 | deps: [AuthClientConfig], 36 | }, 37 | ]; 38 | } 39 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of auth0-angular 3 | */ 4 | 5 | export * from './lib/auth.service'; 6 | export * from './lib/auth.module'; 7 | export * from './lib/auth.guard'; 8 | export * from './lib/auth.interceptor'; 9 | export * from './lib/auth.config'; 10 | export * from './lib/auth.client'; 11 | export * from './lib/auth.state'; 12 | export * from './lib/interfaces'; 13 | export * from './lib/provide'; 14 | export * from './lib/functional'; 15 | export * from './lib/abstract-navigator'; 16 | 17 | export { 18 | AuthorizationParams, 19 | PopupLoginOptions, 20 | PopupConfigOptions, 21 | GetTokenWithPopupOptions, 22 | GetTokenSilentlyOptions, 23 | ICache, 24 | Cacheable, 25 | LocalStorageCache, 26 | InMemoryCache, 27 | IdToken, 28 | User, 29 | GenericError, 30 | TimeoutError, 31 | MfaRequiredError, 32 | PopupTimeoutError, 33 | AuthenticationError, 34 | PopupCancelledError, 35 | MissingRefreshTokenError, 36 | } from '@auth0/auth0-spa-js'; 37 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /projects/auth0-angular/src/useragent.ts: -------------------------------------------------------------------------------- 1 | export default { name: '@auth0/auth0-angular', version: '2.2.1' }; 2 | -------------------------------------------------------------------------------- /projects/auth0-angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "target": "es2020", 15 | "forceConsistentCasingInFileNames": true, 16 | "strict": true, 17 | "noImplicitOverride": true, 18 | "noPropertyAccessFromIndexSignature": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "angularCompilerOptions": { 23 | "enableI18nLegacyMessageIdFormat": false, 24 | "strictInjectionParameters": true, 25 | "strictInputAccessModifiers": true, 26 | "strictTemplates": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/auth0-angular/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "target": "es2015", 7 | "declaration": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": ["dom", "es2018"], 11 | "resolveJsonModule": true 12 | }, 13 | "angularCompilerOptions": { 14 | "skipTemplateCodegen": true, 15 | "strictMetadataEmit": true, 16 | "enableResourceInlining": true 17 | }, 18 | "exclude": ["src/test.ts", "**/*.spec.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /projects/auth0-angular/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "angularCompilerOptions": { 5 | "compilationMode": "partial" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/auth0-angular/tsconfig.schematics.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "lib": ["es2018", "dom"], 5 | "declaration": true, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noEmitOnError": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitAny": true, 11 | "noImplicitThis": true, 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "rootDir": "schematics", 15 | "outDir": "../../dist/auth0-angular/schematics", 16 | "skipDefaultLibCheck": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strictNullChecks": true, 20 | "target": "es6", 21 | "types": ["jasmine", "node"] 22 | }, 23 | "include": ["schematics/**/*"], 24 | "exclude": ["schematics/*/files/**/*"] 25 | } 26 | -------------------------------------------------------------------------------- /projects/auth0-angular/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": ["jest", "node"], 7 | "esModuleInterop": true 8 | }, 9 | "include": ["jest.config.ts", "**/*.spec.ts", "**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /projects/playground/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major version 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /projects/playground/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc.json", 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "parserOptions": { 8 | "project": [ 9 | "projects/playground/tsconfig.app.json", 10 | "projects/playground/tsconfig.spec.json", 11 | "projects/playground/e2e/tsconfig.json" 12 | ], 13 | "createDefaultProgram": true 14 | }, 15 | "rules": { 16 | "@angular-eslint/component-selector": [ 17 | "error", 18 | { 19 | "type": "element", 20 | "prefix": "app", 21 | "style": "kebab-case" 22 | } 23 | ], 24 | "@angular-eslint/directive-selector": [ 25 | "error", 26 | { 27 | "type": "attribute", 28 | "prefix": "app", 29 | "style": "camelCase" 30 | } 31 | ] 32 | } 33 | }, 34 | { 35 | "files": ["*.html"], 36 | "rules": {} 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /projects/playground/e2e/integration/playground.cy.ts: -------------------------------------------------------------------------------- 1 | const EMAIL = Cypress.env('USER_EMAIL') || 'testing'; 2 | const PASSWORD = Cypress.env('USER_PASSWORD') || 'testing'; 3 | 4 | if (!EMAIL || !PASSWORD) { 5 | throw new Error( 6 | 'You must provide CYPRESS_USER_EMAIL and CYPRESS_USER_PASSWORD environment variables' 7 | ); 8 | } 9 | 10 | const loginToAuth0 = () => { 11 | cy.get('.login-card') 12 | .should('have.length', 1) 13 | .then(($form) => { 14 | cy.get('input[name=login]').clear().type(EMAIL); 15 | cy.get('input[name=password]').clear().type(PASSWORD); 16 | cy.get('.login-submit').click(); 17 | cy.get('.login-submit').click(); 18 | }); 19 | }; 20 | 21 | const fixCookies = () => { 22 | // Temporary fix for https://github.com/cypress-io/cypress/issues/6375 23 | if (Cypress.isBrowser('firefox')) { 24 | cy.getCookies({ log: false }).then((cookies) => 25 | cookies.forEach((cookie) => cy.clearCookie(cookie.name, { log: false })) 26 | ); 27 | cy.log('clearCookies'); 28 | } else { 29 | cy.clearCookies(); 30 | } 31 | }; 32 | 33 | describe('Smoke tests', () => { 34 | afterEach(fixCookies); 35 | 36 | it('shows default logged out options', () => { 37 | cy.visit('/'); 38 | cy.get('#login').should('be.visible'); 39 | cy.get('[data-cy=login-redirect]').should('be.checked'); 40 | cy.get('[data-cy=login-popup]').should('not.be.checked'); 41 | }); 42 | 43 | it('shows default logged in options', () => { 44 | cy.visit('/'); 45 | cy.get('#login').should('be.visible').click(); 46 | 47 | loginToAuth0(); 48 | 49 | cy.get('[data-cy=logout-localOnly]').should('not.be.checked'); 50 | cy.get('[data-cy=logout-federated]').should('not.be.checked'); 51 | cy.get('[data-cy=accessToken-ignoreCache]').should('not.be.checked'); 52 | cy.get('[data-cy=accessToken-silently]').should('be.checked'); 53 | cy.get('[data-cy=accessToken-popup]').should('not.be.checked'); 54 | 55 | cy.get('#logout').should('be.visible').click(); 56 | cy.get('button[name=logout]').should('be.visible').click(); 57 | }); 58 | 59 | it('do redirect login and show user, access token and appState', () => { 60 | const appState = 'Any Random String'; 61 | 62 | cy.visit('/'); 63 | cy.get('[data-cy=app-state-input]').type(appState); 64 | cy.get('#login').should('be.visible').click(); 65 | cy.url().should('include', 'http://127.0.0.1:4200'); 66 | loginToAuth0(); 67 | cy.get('[data-cy=userProfile]').contains(`"sub": "${EMAIL}"`); 68 | cy.get('[data-cy=idTokenClaims]').contains('__raw'); 69 | cy.get('[data-cy=accessToken]').should('be.empty'); 70 | cy.get('#accessToken').click(); 71 | 72 | cy.get('[data-cy=accessToken]') 73 | .should('not.be.empty') 74 | .invoke('text') 75 | .then((token) => { 76 | cy.get('#accessToken').click(); 77 | cy.get('[data-cy=accessToken]').should('have.text', token); 78 | }); 79 | 80 | cy.get('[data-cy=app-state-result]').should('have.value', appState); 81 | 82 | cy.get('#logout').should('be.visible').click(); 83 | cy.get('button[name=logout]').should('be.visible').click(); 84 | cy.get('#login').should('be.visible'); 85 | }); 86 | 87 | it('do redirect login and get new access token', () => { 88 | cy.visit('/'); 89 | cy.get('#login').should('be.visible').click(); 90 | 91 | cy.url().should('include', 'http://127.0.0.1:4200'); 92 | loginToAuth0(); 93 | 94 | cy.get('[data-cy=userProfile]').contains(`"sub": "${EMAIL}"`); 95 | 96 | cy.get('[data-cy=accessToken]').should('be.empty'); 97 | cy.get('[data-cy=accessToken-ignoreCache]').check(); 98 | cy.get('#accessToken').click(); 99 | cy.get('[data-cy=accessToken]') 100 | .should('not.be.empty') 101 | .invoke('text') 102 | .then((token) => { 103 | cy.get('#accessToken').click(); 104 | cy.get('[data-cy=accessToken]') 105 | .should('not.be.empty') 106 | .and('not.have.text', token); 107 | }); 108 | 109 | cy.get('#logout').should('be.visible').click(); 110 | cy.get('button[name=logout]').should('be.visible').click(); 111 | cy.get('#login').should('be.visible'); 112 | }); 113 | 114 | it('do local logout (auth0 session remains valid)', () => { 115 | cy.visit('/'); 116 | cy.get('#login').should('be.visible').click(); 117 | 118 | cy.url().should('include', 'http://127.0.0.1:4200'); 119 | loginToAuth0(); 120 | 121 | cy.get('[data-cy=logout-localOnly]').check(); 122 | cy.get('#logout').click(); 123 | 124 | cy.get('#login').should('be.visible').click(); 125 | cy.url().should('include', 'http://127.0.0.1:4200'); 126 | 127 | cy.get('#logout').should('be.visible').click(); 128 | cy.get('button[name=logout]').should('be.visible').click(); 129 | cy.get('#login').should('be.visible'); 130 | }); 131 | 132 | it('do regular logout (auth0 session is cleared)', () => { 133 | cy.visit('/'); 134 | cy.get('#login').should('be.visible').click(); 135 | 136 | cy.url().should('include', 'http://127.0.0.1:4200'); 137 | loginToAuth0(); 138 | 139 | cy.get('#logout').click(); 140 | cy.get('button[name=logout]').should('be.visible').click(); 141 | 142 | cy.get('#login').should('be.visible').click(); 143 | cy.url().should('include', 'http://127.0.0.1:4200'); 144 | cy.get('.auth0-lock-last-login-pane').should('not.exist'); 145 | loginToAuth0(); 146 | 147 | cy.get('#logout').should('be.visible').click(); 148 | cy.get('button[name=logout]').should('be.visible').click(); 149 | cy.get('#login').should('be.visible'); 150 | }); 151 | 152 | it('should protect a route and return to path after login', () => { 153 | cy.visit('/'); 154 | cy.get('[data-cy=protected]').should('not.exist'); 155 | cy.visit('/protected'); 156 | 157 | cy.url().should('include', 'http://127.0.0.1:4200'); 158 | loginToAuth0(); 159 | 160 | cy.url().should('include', '/protected'); 161 | cy.get('[data-cy=protected]').should('be.visible'); 162 | cy.get('#logout').click(); 163 | cy.get('button[name=logout]').should('be.visible').click(); 164 | }); 165 | 166 | it('should see public route content without logging in', () => { 167 | cy.visit('/'); 168 | cy.get('#login').should('be.visible'); 169 | 170 | cy.get('[data-cy=unprotected]').should('be.visible'); 171 | cy.get('[data-cy=protected]').should('not.exist'); 172 | }); 173 | 174 | it('should see child route content without logging in', () => { 175 | cy.visit('/child'); 176 | cy.get('#login').should('be.visible'); 177 | 178 | cy.get('[data-cy=child-route]').should('be.visible'); 179 | }); 180 | 181 | it('should protect the nested child route and return to the right place after login', () => { 182 | cy.visit('/'); 183 | cy.get('[data-cy=nested-child-route]').should('not.exist'); 184 | cy.visit('/child/nested'); 185 | 186 | cy.url().should('include', 'http://127.0.0.1:4200'); 187 | loginToAuth0(); 188 | 189 | cy.url().should('include', '/child/nested'); 190 | cy.get('[data-cy=nested-child-route]').should('be.visible'); 191 | cy.get('#logout').click(); 192 | cy.get('button[name=logout]').should('be.visible').click(); 193 | }); 194 | 195 | it('should not navigate to the lazy loaded module when not authenticated', () => { 196 | cy.visit('/lazy'); 197 | cy.get('[data-cy=lazy-module]').should('not.exist'); 198 | }); 199 | 200 | it('should show lazy module content when authenticated', () => { 201 | cy.visit('/'); 202 | cy.get('#login').should('be.visible').click(); 203 | loginToAuth0(); 204 | cy.get('#logout').should('be.visible'); 205 | cy.visit('/lazy'); 206 | cy.get('[data-cy=lazy-module]').should('be.visible'); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /projects/playground/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es5", 5 | "lib": ["es5", "dom"], 6 | "types": ["cypress"] 7 | }, 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /projects/playground/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'playground', 4 | preset: 'jest-preset-angular', 5 | setupFilesAfterEnv: ['/src/test-setup.ts'], 6 | globals: { 7 | 'ts-jest': { 8 | tsconfig: '/tsconfig.spec.json', 9 | stringifyContentPathRegex: '\\.(html|svg)$', 10 | }, 11 | }, 12 | transform: { 13 | '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular', 14 | }, 15 | transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], 16 | snapshotSerializers: [ 17 | 'jest-preset-angular/build/serializers/no-ng-attributes', 18 | 'jest-preset-angular/build/serializers/ng-snapshot', 19 | 'jest-preset-angular/build/serializers/html-comment', 20 | ], 21 | testPathIgnorePatterns: ['/e2e'], 22 | collectCoverage: false, 23 | coveragePathIgnorePatterns: ['/src'], 24 | }; 25 | -------------------------------------------------------------------------------- /projects/playground/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma'), 14 | ], 15 | client: { 16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageReporter: { 19 | dir: require('path').join(__dirname, './../../coverage/playground'), 20 | subdir: '.', 21 | reporters: [ 22 | { type: 'html' }, 23 | { type: 'lcovonly' }, 24 | { type: 'text-summary' }, 25 | ], 26 | }, 27 | reporters: ['progress', 'kjhtml'], 28 | port: 9876, 29 | colors: true, 30 | logLevel: config.LOG_INFO, 31 | autoWatch: true, 32 | browsers: ['Chrome'], 33 | customLaunchers: { 34 | ChromeHeadlessCI: { 35 | base: 'ChromeHeadless', 36 | flags: ['--no-sandbox'], 37 | }, 38 | }, 39 | singleRun: false, 40 | restartOnFileChange: true, 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /projects/playground/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ProtectedComponent } from './components/protected.component'; 4 | import { AuthGuard } from 'projects/auth0-angular/src/lib/auth.guard'; 5 | import { UnprotectedComponent } from './components/unprotected.component'; 6 | import { ChildRouteComponent } from './components/child-route.component'; 7 | import { NestedChildRouteComponent } from './components/nested-child-route.component'; 8 | import { ErrorComponent } from './components/error.component'; 9 | 10 | const routes: Routes = [ 11 | { 12 | path: '', 13 | component: UnprotectedComponent, 14 | pathMatch: 'full', 15 | }, 16 | { 17 | path: 'child', 18 | component: ChildRouteComponent, 19 | canActivateChild: [AuthGuard], 20 | children: [{ path: 'nested', component: NestedChildRouteComponent }], 21 | }, 22 | { 23 | path: 'protected', 24 | component: ProtectedComponent, 25 | canActivate: [AuthGuard], 26 | }, 27 | { 28 | path: 'lazy', 29 | canLoad: [AuthGuard], 30 | loadChildren: () => 31 | import('./lazy-module.module').then((m) => m.LazyModuleModule), 32 | }, 33 | { 34 | path: 'error', 35 | component: ErrorComponent, 36 | }, 37 | ]; 38 | 39 | @NgModule({ 40 | imports: [RouterModule.forRoot(routes)], 41 | exports: [RouterModule], 42 | }) 43 | export class AppRoutingModule {} 44 | -------------------------------------------------------------------------------- /projects/playground/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0/auth0-angular/d366f37fc36bfbadfcc40dac6ad4eff1f10d9bfc/projects/playground/src/app/app.component.css -------------------------------------------------------------------------------- /projects/playground/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |

{{ 'Auth0 Angular Playground' | uppercase }}

3 |

4 | SDK initialized = {{ (isLoading$ | async) === false }} 5 |

6 | 7 |
8 |

Authentication

9 | 52 | 53 |
54 |
55 | 63 | 64 | 72 | 73 |
74 |
75 | 76 |
77 |

Last error

78 | {{ err.message }} 79 |
80 | 81 |
82 |

Call API

83 | 84 |
85 | 86 |
87 |

Artifacts

88 |
    89 |
  • 90 |

    User Profile: Subset of the ID token claims

    91 | 94 |
  • 95 |
  • 96 |

    ID token claims:

    97 | 107 |
  • 108 |
  • 109 |

    110 | Access Token: Select a mode and click the button to retrieve the 111 | token. 112 |

    113 |
    114 | 123 | 124 | 133 | 134 | 143 | 146 |
    147 |
    148 | 151 | 152 |
    153 |
    154 | 155 | 162 |
  • 163 |
164 |
165 | 166 |
167 |

Test Auth Guard

168 | Unprotected Route | 169 | Protected Route | 170 | Lazy-loaded route | 171 | Child route | 172 | Nested child route 173 | 174 |
175 |
176 | -------------------------------------------------------------------------------- /projects/playground/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/dot-notation */ 2 | import { TestBed, ComponentFixture } from '@angular/core/testing'; 3 | import { RouterTestingModule } from '@angular/router/testing'; 4 | import { AppComponent } from './app.component'; 5 | import { AuthService } from '../../../auth0-angular/src/lib/auth.service'; 6 | import { BehaviorSubject, of, ReplaySubject } from 'rxjs'; 7 | import { ReactiveFormsModule } from '@angular/forms'; 8 | import { HttpClientModule } from '@angular/common/http'; 9 | import { expect } from '@jest/globals'; 10 | 11 | describe('AppComponent', () => { 12 | let authMock: AuthService; 13 | let fixture: ComponentFixture; 14 | let component: AppComponent; 15 | let ne: HTMLElement; 16 | 17 | beforeEach(() => { 18 | authMock = { 19 | loginWithRedirect: jest.fn().mockReturnValue(null), 20 | loginWithPopup: jest.fn().mockReturnValue(null), 21 | logout: jest.fn().mockReturnValue(null), 22 | getAccessTokenSilently: jest.fn().mockReturnValue(null), 23 | getAccessTokenWithPopup: jest.fn().mockReturnValue(null), 24 | 25 | user$: new BehaviorSubject(null), 26 | isLoading$: new BehaviorSubject(true), 27 | isAuthenticated$: new BehaviorSubject(false), 28 | appState$: new ReplaySubject(), 29 | } as any; 30 | 31 | TestBed.configureTestingModule({ 32 | imports: [RouterTestingModule, ReactiveFormsModule, HttpClientModule], 33 | declarations: [AppComponent], 34 | providers: [ 35 | { 36 | provide: AuthService, 37 | useValue: authMock, 38 | }, 39 | ], 40 | }).compileComponents(); 41 | 42 | fixture = TestBed.createComponent(AppComponent); 43 | component = fixture.componentInstance; 44 | ne = fixture.nativeElement; 45 | fixture.detectChanges(); 46 | }); 47 | 48 | describe('when initialized', () => { 49 | it('should create the app', () => { 50 | expect(component).toBeTruthy(); 51 | }); 52 | 53 | it('should render title', () => { 54 | const h1 = ne.querySelector('h1'); 55 | expect(h1?.textContent).toContain('AUTH0 ANGULAR PLAYGROUND'); 56 | }); 57 | 58 | it('should render SDK loading status', () => { 59 | const loadingIndicator = ne.querySelector('p.status-indicator'); 60 | expect(loadingIndicator?.textContent).toContain( 61 | 'SDK initialized = false' 62 | ); 63 | 64 | const loading = authMock.isLoading$ as BehaviorSubject; 65 | loading.next(false); 66 | fixture.detectChanges(); 67 | 68 | expect(loadingIndicator?.textContent).toContain('SDK initialized = true'); 69 | }); 70 | 71 | it('should show controls when SDK finishes loading', () => { 72 | let actions = ne.querySelector('.actions'); 73 | expect(actions).toBeNull(); 74 | 75 | const loading = authMock.isLoading$ as BehaviorSubject; 76 | loading.next(false); 77 | fixture.detectChanges(); 78 | 79 | actions = ne.querySelector('.actions'); 80 | expect(actions).toBeTruthy(); 81 | }); 82 | }); 83 | 84 | describe('when user is authenticated', () => { 85 | beforeEach(() => { 86 | const loading = authMock.isLoading$ as BehaviorSubject; 87 | const authenticated = authMock.isAuthenticated$ as BehaviorSubject; 88 | const user = authMock.user$ as BehaviorSubject; 89 | 90 | loading.next(false); 91 | authenticated.next(true); 92 | user.next({ name: 'John', lastname: 'Doe' }); 93 | fixture.detectChanges(); 94 | }); 95 | 96 | it('should hide login options', () => { 97 | const wrapLogin = ne.querySelector('.login-wrapper'); 98 | expect(wrapLogin).toBeNull(); 99 | }); 100 | 101 | it('should show logout options', () => { 102 | const wrapLogout = ne.querySelector('.logout-wrapper'); 103 | expect(wrapLogout).toBeTruthy(); 104 | 105 | const btnLogout = ne.querySelector('#logout'); 106 | expect(btnLogout).toBeTruthy(); 107 | }); 108 | 109 | it('should logout with default options', () => { 110 | const form = component.logoutOptionsForm.controls; 111 | form['localOnly'].setValue(false); 112 | form['federated'].setValue(false); 113 | 114 | const btnLogout = ne.querySelector('#logout') as HTMLButtonElement; 115 | btnLogout.click(); 116 | fixture.detectChanges(); 117 | 118 | expect(authMock.logout).toHaveBeenCalledWith({ 119 | openUrl: undefined, 120 | logoutParams: { 121 | federated: false, 122 | returnTo: 'http://localhost', 123 | }, 124 | }); 125 | }); 126 | 127 | it('should logout with federated', () => { 128 | const form = component.logoutOptionsForm.controls; 129 | form['localOnly'].setValue(false); 130 | form['federated'].setValue(true); 131 | 132 | const btnLogout = ne.querySelector('#logout') as HTMLButtonElement; 133 | btnLogout.click(); 134 | fixture.detectChanges(); 135 | 136 | expect(authMock.logout).toHaveBeenCalledWith({ 137 | openUrl: undefined, 138 | logoutParams: { 139 | federated: true, 140 | returnTo: 'http://localhost', 141 | }, 142 | }); 143 | }); 144 | 145 | it('should logout with localOnly', () => { 146 | const form = component.logoutOptionsForm.controls; 147 | form['localOnly'].setValue(true); 148 | form['federated'].setValue(false); 149 | 150 | const btnLogout = ne.querySelector('#logout') as HTMLButtonElement; 151 | btnLogout.click(); 152 | fixture.detectChanges(); 153 | 154 | expect(authMock.logout).toHaveBeenCalledWith( 155 | expect.objectContaining({ 156 | openUrl: false, 157 | logoutParams: { 158 | federated: false, 159 | returnTo: 'http://localhost', 160 | }, 161 | }) 162 | ); 163 | }); 164 | 165 | it('should show user profile', () => { 166 | const divProfile = ne.querySelectorAll('.artifacts-wrapper .artifact')[0]; 167 | expect(divProfile.querySelector('p')?.textContent).toContain( 168 | 'User Profile: Subset of the ID token claims' 169 | ); 170 | const userValue = JSON.parse( 171 | divProfile.querySelector('textarea')?.textContent ?? '' 172 | ); 173 | expect(userValue).toEqual({ name: 'John', lastname: 'Doe' }); 174 | }); 175 | 176 | it('should show empty access token by default', () => { 177 | const divToken = ne.querySelectorAll('.artifacts-wrapper .artifact')[1]; 178 | expect(divToken.querySelector('p')?.textContent).toContain( 179 | 'Access Token: Select a mode and click the button to retrieve the token.' 180 | ); 181 | const tokenContent = divToken.querySelector('textarea')?.textContent; 182 | expect(tokenContent).toEqual(''); 183 | }); 184 | 185 | it('should get access token silently', () => { 186 | const divToken = ne.querySelectorAll('.artifacts-wrapper .artifact')[1]; 187 | expect(divToken.querySelector('p')?.textContent).toContain( 188 | 'Access Token' 189 | ); 190 | const form = component.accessTokenOptionsForm.controls; 191 | form['usePopup'].setValue(false); 192 | ((authMock.getAccessTokenSilently as unknown) as jest.SpyInstance).mockReturnValue( 193 | of('access token silently') 194 | ); 195 | 196 | const btnRefresh = divToken.querySelector('button'); 197 | btnRefresh?.click(); 198 | fixture.detectChanges(); 199 | 200 | expect(authMock.getAccessTokenSilently).toHaveBeenCalledWith({ 201 | cacheMode: 'on', 202 | }); 203 | const tokenContent = divToken.querySelector('textarea')?.textContent; 204 | expect(tokenContent).toEqual('access token silently'); 205 | }); 206 | 207 | it('should get access token silently', () => { 208 | const divToken = ne.querySelectorAll('.artifacts-wrapper .artifact')[1]; 209 | expect(divToken.querySelector('p')?.textContent).toContain( 210 | 'Access Token' 211 | ); 212 | const form = component.accessTokenOptionsForm.controls; 213 | form['usePopup'].setValue(false); 214 | form['ignoreCache'].setValue(false); 215 | ((authMock.getAccessTokenSilently as unknown) as jest.SpyInstance).mockReturnValue( 216 | of('access token silently') 217 | ); 218 | 219 | const btnRefresh = divToken.querySelector('button'); 220 | btnRefresh?.click(); 221 | fixture.detectChanges(); 222 | 223 | expect(authMock.getAccessTokenSilently).toHaveBeenCalledWith({ 224 | cacheMode: 'on', 225 | }); 226 | const tokenContent = divToken.querySelector('textarea')?.textContent; 227 | expect(tokenContent).toEqual('access token silently'); 228 | }); 229 | 230 | it('should get access token silently ignoring cache', () => { 231 | const divToken = ne.querySelectorAll('.artifacts-wrapper .artifact')[1]; 232 | expect(divToken.querySelector('p')?.textContent).toContain( 233 | 'Access Token' 234 | ); 235 | const form = component.accessTokenOptionsForm.controls; 236 | form['usePopup'].setValue(false); 237 | form['ignoreCache'].setValue(true); 238 | ((authMock.getAccessTokenSilently as unknown) as jest.SpyInstance).mockReturnValue( 239 | of('access token silently') 240 | ); 241 | 242 | const btnRefresh = divToken.querySelector('button'); 243 | btnRefresh?.click(); 244 | fixture.detectChanges(); 245 | 246 | expect(authMock.getAccessTokenSilently).toHaveBeenCalledWith({ 247 | cacheMode: 'off', 248 | }); 249 | const tokenContent = divToken.querySelector('textarea')?.textContent; 250 | expect(tokenContent).toEqual('access token silently'); 251 | }); 252 | 253 | it('should get access token with popup', () => { 254 | const divToken = ne.querySelectorAll('.artifacts-wrapper .artifact')[1]; 255 | expect(divToken.querySelector('p')?.textContent).toContain( 256 | 'Access Token' 257 | ); 258 | const form = component.accessTokenOptionsForm.controls; 259 | form['usePopup'].setValue(true); 260 | ((authMock.getAccessTokenWithPopup as unknown) as jest.SpyInstance).mockReturnValue( 261 | of('access token popup') 262 | ); 263 | 264 | const btnRefresh = divToken.querySelector('button'); 265 | btnRefresh?.click(); 266 | fixture.detectChanges(); 267 | 268 | expect(authMock.getAccessTokenWithPopup).toHaveBeenCalledWith(); 269 | const tokenContent = divToken.querySelector('textarea')?.textContent; 270 | expect(tokenContent).toEqual('access token popup'); 271 | }); 272 | }); 273 | 274 | describe('when user is not authenticated', () => { 275 | beforeEach(() => { 276 | const loading = authMock.isLoading$ as BehaviorSubject; 277 | const authenticated = authMock.isAuthenticated$ as BehaviorSubject; 278 | 279 | loading.next(false); 280 | authenticated.next(false); 281 | fixture.detectChanges(); 282 | }); 283 | 284 | it('should hide logout options', () => { 285 | const wrapLogout = ne.querySelector('.logout-wrapper'); 286 | expect(wrapLogout).toBeNull(); 287 | }); 288 | 289 | it('should show login options', () => { 290 | const wrapLogin = ne.querySelector('.login-wrapper'); 291 | expect(wrapLogin).toBeTruthy(); 292 | 293 | const btnLogin = ne.querySelector('#login'); 294 | expect(btnLogin).toBeTruthy(); 295 | }); 296 | 297 | it('should login with redirect', () => { 298 | const appStateValue = 'Value to Preserve'; 299 | 300 | const wrapLogin = ne.querySelector('.login-wrapper'); 301 | const form = component.loginOptionsForm.controls; 302 | form['usePopup'].setValue(false); 303 | form['appStateInput'].setValue(appStateValue); 304 | 305 | const btnRefresh = wrapLogin?.querySelector('button'); 306 | btnRefresh?.click(); 307 | fixture.detectChanges(); 308 | 309 | expect(authMock.loginWithRedirect).toHaveBeenCalledWith({ 310 | appState: { 311 | myValue: appStateValue, 312 | }, 313 | authorizationParams: {}, 314 | }); 315 | }); 316 | 317 | it('should login with popup', () => { 318 | const wrapLogin = ne.querySelector('.login-wrapper'); 319 | const form = component.loginOptionsForm.controls; 320 | form['usePopup'].setValue(true); 321 | 322 | const btnRefresh = wrapLogin?.querySelector('button'); 323 | btnRefresh?.click(); 324 | fixture.detectChanges(); 325 | 326 | expect(authMock.loginWithPopup).toHaveBeenCalledWith({ 327 | authorizationParams: {}, 328 | }); 329 | }); 330 | }); 331 | }); 332 | -------------------------------------------------------------------------------- /projects/playground/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/dot-notation */ 2 | import { Component, Inject, OnInit } from '@angular/core'; 3 | import { FormGroup, FormControl } from '@angular/forms'; 4 | import { AuthService } from '../../../auth0-angular/src/lib/auth.service'; 5 | import { iif } from 'rxjs'; 6 | import { first } from 'rxjs/operators'; 7 | import { DOCUMENT } from '@angular/common'; 8 | import { HttpClient } from '@angular/common/http'; 9 | import { LogoutOptions } from 'projects/auth0-angular/src/lib/interfaces'; 10 | 11 | @Component({ 12 | selector: 'app-root', 13 | templateUrl: './app.component.html', 14 | styleUrls: ['./app.component.css'], 15 | }) 16 | export class AppComponent implements OnInit { 17 | isAuthenticated$ = this.auth.isAuthenticated$; 18 | isLoading$ = this.auth.isLoading$; 19 | user$ = this.auth.user$; 20 | claims$ = this.auth.idTokenClaims$; 21 | accessToken = ''; 22 | appStateResult = ''; 23 | error$ = this.auth.error$; 24 | 25 | organization = ''; 26 | 27 | loginOptionsForm = new FormGroup({ 28 | appStateInput: new FormControl(''), 29 | usePopup: new FormControl(false), 30 | }); 31 | 32 | logoutOptionsForm = new FormGroup({ 33 | localOnly: new FormControl(false), 34 | federated: new FormControl(false), 35 | }); 36 | 37 | accessTokenOptionsForm = new FormGroup({ 38 | usePopup: new FormControl(false), 39 | ignoreCache: new FormControl(false), 40 | }); 41 | 42 | constructor( 43 | public auth: AuthService, 44 | @Inject(DOCUMENT) private doc: Document, 45 | private httpClient: HttpClient 46 | ) {} 47 | 48 | ngOnInit(): void { 49 | this.auth.appState$.subscribe((appState) => { 50 | this.appStateResult = appState['myValue']; 51 | }); 52 | } 53 | 54 | launchLogin(): void { 55 | const usePopup = this.loginOptionsForm.value.usePopup === true; 56 | if (usePopup) { 57 | this.auth.loginWithPopup({ 58 | authorizationParams: { 59 | ...(this.organization ? { organization: this.organization } : null), 60 | }, 61 | }); 62 | } else { 63 | this.auth.loginWithRedirect({ 64 | authorizationParams: { 65 | ...(this.organization ? { organization: this.organization } : null), 66 | }, 67 | appState: { 68 | myValue: this.loginOptionsForm.value.appStateInput, 69 | }, 70 | }); 71 | } 72 | } 73 | 74 | loginHandleInvitationUrl(): void { 75 | const url = prompt('Your invitation URL'); 76 | 77 | if (url) { 78 | const inviteMatches = url.match(/invitation=([a-zA-Z0-9_]+)/); 79 | const orgMatches = url.match(/organization=([a-zA-Z0-9_]+)/); 80 | 81 | if (orgMatches && inviteMatches) { 82 | this.auth.loginWithRedirect({ 83 | authorizationParams: { 84 | organization: orgMatches[1], 85 | invitation: inviteMatches[1], 86 | }, 87 | }); 88 | } else if (orgMatches) { 89 | this.auth.loginWithRedirect({ 90 | authorizationParams: { 91 | organization: orgMatches[1], 92 | }, 93 | }); 94 | } 95 | } 96 | } 97 | 98 | launchLogout(): void { 99 | const formOptions = this.logoutOptionsForm.value; 100 | const options: LogoutOptions = { 101 | openUrl: formOptions.localOnly === true ? false : undefined, 102 | logoutParams: { 103 | federated: formOptions.federated === true, 104 | returnTo: this.doc.location.origin, 105 | }, 106 | }; 107 | 108 | this.auth.logout(options); 109 | } 110 | 111 | updateAccessToken(): void { 112 | const usePopup = this.accessTokenOptionsForm.value.usePopup === true; 113 | const ignoreCache = this.accessTokenOptionsForm.value.ignoreCache === true; 114 | iif( 115 | () => usePopup, 116 | this.auth.getAccessTokenWithPopup(), 117 | this.auth.getAccessTokenSilently({ 118 | cacheMode: ignoreCache ? 'off' : 'on', 119 | }) 120 | ) 121 | .pipe(first()) 122 | .subscribe((token) => { 123 | if (token) { 124 | this.accessToken = token; 125 | } 126 | }); 127 | } 128 | 129 | callExternalAPI(): void { 130 | this.httpClient 131 | .get('http://localhost:3001/api/external') 132 | .subscribe(console.log, console.error); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /projects/playground/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule, APP_INITIALIZER } from '@angular/core'; 3 | import { 4 | HttpClient, 5 | HttpClientModule, 6 | HTTP_INTERCEPTORS, 7 | HttpBackend, 8 | } from '@angular/common/http'; 9 | import { AuthModule } from 'projects/auth0-angular/src/lib/auth.module'; 10 | import { AuthHttpInterceptor } from 'projects/auth0-angular/src/lib/auth.interceptor'; 11 | import { ReactiveFormsModule } from '@angular/forms'; 12 | import { AppRoutingModule } from './app-routing.module'; 13 | import { AppComponent } from './app.component'; 14 | import { ProtectedComponent } from './components/protected.component'; 15 | import { UnprotectedComponent } from './components/unprotected.component'; 16 | import { ChildRouteComponent } from './components/child-route.component'; 17 | import { NestedChildRouteComponent } from './components/nested-child-route.component'; 18 | import { ErrorComponent } from './components/error.component'; 19 | 20 | import { AuthClientConfig } from 'projects/auth0-angular/src/lib/auth.config'; 21 | 22 | /** 23 | * Provides configuration to the application 24 | * 25 | * @param handler The HttpBackend instance used to instantiate HttpClient manually 26 | * @param config The AuthConfigClient service 27 | */ 28 | const configInitializer = ( 29 | handler: HttpBackend, 30 | config: AuthClientConfig 31 | ): (() => Promise) => () => 32 | new HttpClient(handler) 33 | .get('/assets/config.json') 34 | .toPromise() 35 | .then((loadedConfig: any) => config.set(loadedConfig)); // Set the config that was loaded asynchronously here 36 | 37 | @NgModule({ 38 | declarations: [ 39 | AppComponent, 40 | ProtectedComponent, 41 | UnprotectedComponent, 42 | ChildRouteComponent, 43 | NestedChildRouteComponent, 44 | ErrorComponent, 45 | ], 46 | imports: [ 47 | BrowserModule, 48 | AppRoutingModule, 49 | ReactiveFormsModule, 50 | HttpClientModule, 51 | 52 | // This playground has been configured by default to use dynamic configuration. 53 | // If you wish to specify configuration to `forRoot` directly, uncomment `authConfig` 54 | // here, and comment out the APP_INITIALIZER config in the providers array below. 55 | AuthModule.forRoot(/* authConfig */), 56 | ], 57 | providers: [ 58 | { 59 | provide: APP_INITIALIZER, 60 | useFactory: configInitializer, 61 | deps: [HttpBackend, AuthClientConfig], 62 | multi: true, 63 | }, 64 | { provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true }, 65 | ], 66 | bootstrap: [AppComponent], 67 | }) 68 | export class AppModule {} 69 | -------------------------------------------------------------------------------- /projects/playground/src/app/components/child-route.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-child-route', 5 | template: ` 6 |

7 | child-route works! 8 |

9 | 10 | 11 | `, 12 | styles: [], 13 | }) 14 | export class ChildRouteComponent implements OnInit { 15 | constructor() {} 16 | 17 | ngOnInit(): void {} 18 | } 19 | -------------------------------------------------------------------------------- /projects/playground/src/app/components/error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-error', 5 | template: '

Error!

', 6 | styles: [], 7 | }) 8 | export class ErrorComponent {} 9 | -------------------------------------------------------------------------------- /projects/playground/src/app/components/lazy-module.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-lazy-module', 5 | template: '

lazy-module works!

', 6 | styles: [], 7 | }) 8 | export class LazyModuleComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /projects/playground/src/app/components/nested-child-route.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-nested-child-route', 5 | template: ` 6 |

7 | Nested child-route works! 8 |

9 | `, 10 | styles: [], 11 | }) 12 | export class NestedChildRouteComponent implements OnInit { 13 | constructor() {} 14 | 15 | ngOnInit(): void {} 16 | } 17 | -------------------------------------------------------------------------------- /projects/playground/src/app/components/protected.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-protected', 5 | template: '

This route is protected!

', 6 | styles: [], 7 | }) 8 | export class ProtectedComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /projects/playground/src/app/components/unprotected.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-unprotected', 5 | template: '

This route is unprotected!

', 6 | styles: [], 7 | }) 8 | export class UnprotectedComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit(): void {} 12 | } 13 | -------------------------------------------------------------------------------- /projects/playground/src/app/lazy-module-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { LazyModuleComponent } from './components/lazy-module.component'; 5 | 6 | const routes: Routes = [{ path: '', component: LazyModuleComponent }]; 7 | 8 | @NgModule({ 9 | imports: [RouterModule.forChild(routes)], 10 | exports: [RouterModule], 11 | }) 12 | export class LazyModuleRoutingModule {} 13 | -------------------------------------------------------------------------------- /projects/playground/src/app/lazy-module.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { LazyModuleRoutingModule } from './lazy-module-routing.module'; 5 | import { LazyModuleComponent } from './components/lazy-module.component'; 6 | 7 | @NgModule({ 8 | declarations: [LazyModuleComponent], 9 | imports: [CommonModule, LazyModuleRoutingModule], 10 | }) 11 | export class LazyModuleModule {} 12 | -------------------------------------------------------------------------------- /projects/playground/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0/auth0-angular/d366f37fc36bfbadfcc40dac6ad4eff1f10d9bfc/projects/playground/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/playground/src/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientId": "wLSIP47wM39wKdDmOj6Zb5eSEw3JVhVp", 3 | "domain": "brucke.auth0.com", 4 | "authorizationParams": { 5 | "audience": "http://localhost/", 6 | "redirect_uri": "http://localhost:4200" 7 | }, 8 | "errorPath": "/error", 9 | "httpInterceptor": { 10 | "allowedList": [ 11 | { 12 | "uri": "http://localhost:3001/api/external" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /projects/playground/src/config/local/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "clientId": "testing", 3 | "domain": "http://127.0.0.1:4200", 4 | "authorizationParams": { 5 | "audience": "test", 6 | "redirect_uri": "http://127.0.0.1:4200" 7 | }, 8 | "errorPath": "/error", 9 | "httpInterceptor": { 10 | "allowedList": [ 11 | { 12 | "uri": "http://localhost:3001/api/external" 13 | } 14 | ] 15 | }, 16 | "useFormData": true 17 | } 18 | -------------------------------------------------------------------------------- /projects/playground/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/playground/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/playground/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auth0/auth0-angular/d366f37fc36bfbadfcc40dac6ad4eff1f10d9bfc/projects/playground/src/favicon.ico -------------------------------------------------------------------------------- /projects/playground/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Playground 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/playground/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /projects/playground/src/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/interaction": { 3 | "target": "http://127.0.0.1:3000", 4 | "secure": false 5 | }, 6 | "/authorize": { 7 | "target": "http://127.0.0.1:3000", 8 | "secure": false 9 | }, 10 | "/oauth/token": { 11 | "target": "http://127.0.0.1:3000", 12 | "secure": false 13 | }, 14 | "/v2/logout": { 15 | "target": "http://127.0.0.1:3000", 16 | "secure": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /projects/playground/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /projects/playground/src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular/setup-jest'; 2 | -------------------------------------------------------------------------------- /projects/playground/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": ["src/main.ts", "src/polyfills.ts"], 9 | "include": ["src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /projects/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "target": "es2020", 15 | "forceConsistentCasingInFileNames": true, 16 | "strict": true, 17 | "noImplicitOverride": true, 18 | "noPropertyAccessFromIndexSignature": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "angularCompilerOptions": { 23 | "enableI18nLegacyMessageIdFormat": false, 24 | "strictInjectionParameters": true, 25 | "strictInputAccessModifiers": true, 26 | "strictTemplates": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/playground/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": ["jest", "node"], 7 | "esModuleInterop": true 8 | }, 9 | "include": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /scripts/exec.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec; 2 | 3 | module.exports = (cmd) => { 4 | return new Promise((resolve, reject) => { 5 | exec(cmd, (error, stdout, stderr) => { 6 | if (error) { 7 | reject(error); 8 | } 9 | resolve(stdout ? stdout : stderr); 10 | }); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /scripts/oidc-provider.js: -------------------------------------------------------------------------------- 1 | const { Provider, interactionPolicy } = require('oidc-provider'); 2 | const { base, Prompt, Check } = interactionPolicy; 3 | 4 | const policy = base(); 5 | policy.add( 6 | new Prompt( 7 | { name: 'noop', requestable: false }, 8 | new Check('foo', 'bar', (ctx) => { 9 | if ( 10 | ctx.query && 11 | ctx.query.scope && 12 | ctx.query.scope.includes('offline_access') 13 | ) { 14 | ctx.oidc.params.scope = `${ctx.oidc.params.scope} offline_access`; 15 | } 16 | return Check.NO_NEED_TO_PROMPT; 17 | }) 18 | ), 19 | 0 20 | ); 21 | 22 | const config = { 23 | clients: [ 24 | { 25 | client_id: 'testing', 26 | redirect_uris: ['http://127.0.0.1:4200', 'http://localhost:4200'], 27 | token_endpoint_auth_method: 'none', 28 | grant_types: ['authorization_code', 'refresh_token'], 29 | }, 30 | ], 31 | routes: { 32 | authorization: '/authorize', // lgtm [js/hardcoded-credentials] 33 | token: '/oauth/token', 34 | end_session: '/v2/logout', 35 | }, 36 | scopes: ['openid', 'offline_access'], 37 | clientBasedCORS(ctx, origin, client) { 38 | return true; 39 | }, 40 | features: { 41 | webMessageResponseMode: { 42 | enabled: true, 43 | }, 44 | }, 45 | rotateRefreshToken: true, 46 | interactions: { 47 | policy, 48 | }, 49 | }; 50 | 51 | function createApp() { 52 | const issuer = `http://127.0.0.1:4200/`; 53 | const provider = new Provider(issuer, config); 54 | 55 | provider.use(async (ctx, next) => { 56 | await next(); 57 | 58 | if (ctx.oidc && ctx.oidc.route === 'end_session_success') { 59 | ctx.redirect('http://127.0.0.1:4200'); 60 | } 61 | }); 62 | 63 | provider.listen(3000, () => { 64 | console.log( 65 | 'oidc-provider listening on port 3000, check http://localhost:3000/.well-known/openid-configuration' 66 | ); 67 | }); 68 | } 69 | 70 | createApp(); 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitReturns": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "sourceMap": true, 11 | "declaration": false, 12 | "downlevelIteration": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "node", 15 | "importHelpers": true, 16 | "target": "es2015", 17 | "module": "es2020", 18 | "lib": ["es2018", "dom"], 19 | "paths": { 20 | "auth0-angular": [ 21 | "dist/auth0-angular/auth0-angular", 22 | "dist/auth0-angular" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["./projects/auth0-angular/src/public-api.ts"], 3 | "exclude": ["**/*.spec.ts"], 4 | "tsconfig": "./projects/auth0-angular/tsconfig.lib.json", 5 | "out": "docs", 6 | "excludeExternals": false, 7 | "excludePrivate": true, 8 | "hideGenerator": true, 9 | "readme": "./README.md", 10 | "visibilityFilters": { 11 | "protected": false, 12 | "inherited": true, 13 | "external": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------