├── .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 |
102 |
{{ user.name }}
103 |
{{ user.email }}
104 |
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 | 
2 |
3 | A library for integrating [Auth0](https://auth0.com) into an Angular application.
4 |
5 | 
6 | [](https://codecov.io/gh/auth0/auth0-angular)
7 | 
8 | [](https://opensource.org/licenses/MIT)
9 | [](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 |
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 |