Skip to content

Latest commit

 

History

History
2327 lines (1859 loc) · 80.2 KB

File metadata and controls

2327 lines (1859 loc) · 80.2 KB

Examples using auth0-angular

Add login to your application

To log the user into the application, inject the AuthService and call its loginWithRedirect method.

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(public auth: AuthService) {}

  loginWithRedirect() {
    this.auth.loginWithRedirect();
  }

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 option.

Add logout to your application

To log the user out of your application, call the logout method on AuthService from anywhere inside your application, such as a component:

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(public auth: AuthService) {}

  logout() {
    this.auth.logout();
  }

Checking if a user is authenticated

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.

<ng-container
  *ngIf="auth.isAuthenticated$ | async; then loggedIn; else loggedOut">
</ng-container>

<ng-template #loggedIn>
  <button (click)="logout()">
    Log out
  </button>
</ng-template>

<ng-template #loggedOut>
  <button (click)="loginWithRedirect()">
    Log in
  </button>
</ng-template>

Display the user profile

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:

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({
  selector: 'app-profile',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class ProfileComponent {
  user$ = this.auth.user$;

  constructor(public auth: AuthService) {}

You can then access the component's user$ observable from within your template.

<ul *ngIf="user$ | async as user">
  <li>{{ user.name }}</li>
  <li>{{ user.email }}</li>
</ul>

Protect a route

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.

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:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './unprotected/unprotected.component';
import { ProtectedComponent } from './protected/protected.component';
import { AuthGuard } from '@auth0/auth0-angular';

const routes: Routes = [
  {
    path: 'protected',
    component: ProtectedComponent,
    canActivate: [AuthGuard],
  },
  {
    path: '',
    component: HomeComponent,
    pathMatch: 'full',
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Call an API

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.

Specify the audience

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.

import { NgModule } from '@angular/core';
import { AuthModule } from '@auth0/auth0-angular';

@NgModule({
  // ...
  imports: [
    AuthModule.forRoot({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      authorizationParams: {
        audience: 'YOUR_AUTH0_API_IDENTIFIER',
      },
    }),
  ],
  // ...
})
export class AppModule {}

Register AuthHttpInterceptor

First, register the interceptor with your application module, along with the HttpClientModule.

import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthHttpInterceptor } from '@auth0/auth0-angular';

@NgModule({
  // ...
  imports: [
    HttpClientModule,
    AuthModule.forRoot(...),
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthHttpInterceptor, multi: true },
  ],
  // ...
})

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.

Configure AuthHttpInterceptor to attach access tokens

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.

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.

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.

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.

Here are some examples:

import { HttpMethod } from '@auth0/auth0-angular';

// Modify your existing SDK configuration to include the httpInterceptor config
AuthModule.forRoot({
  ...
  // The AuthHttpInterceptor configuration
  httpInterceptor: {
    allowedList: [
      // Attach access tokens to any calls to '/api' (exact match)
      '/api',

      // Attach access tokens to any calls that start with '/api/'
      '/api/*',

      // Match anything starting with /api/products, but also allow for anonymous users.
      {
        uri: '/api/products/*',
        allowAnonymous: true,
      },

      // Match anything starting with /api/accounts, but also specify the audience and scope the attached
      // access token must have
      {
        uri: '/api/accounts/*',
        tokenOptions: {
          authorizationParams: {
            audience: 'http://my-api/',
            scope: 'read:accounts',
          }
        },
      },

      // Matching on HTTP method
      {
        uri: '/api/orders',
        httpMethod: HttpMethod.Post,
        tokenOptions: {
          authorizationParams: {
            audience: 'http://my-api/',
            scope: 'write:orders',
          }
        },
      },

      // Using an absolute URI
      {
        uri: 'https://your-domain.auth0.com/api/v2/users',
        tokenOptions: {
          authorizationParams: {
            audience: 'https://your-domain.com/api/v2/',
            scope: 'read:users',
          }
        },
      },
    ],
  },
});

Note: Under the hood, tokenOptions is passed as-is to the getTokenSilently method on the underlying SDK, so all the same options apply here.

Uri matching

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) and returns a boolean. If this function returns true, then an access token is attached to the request in the "Authorization" header. If it returns false, the request proceeds without the access token attached.

AuthModule.forRoot({
  // ...
  httpInterceptor: {
    allowedList: [
      {
        uriMatcher: (uri) => uri.indexOf('/api/orders') > -1,
        httpMethod: HttpMethod.Post,
        tokenOptions: {
          authorizationParams: {
            audience: 'http://my-api/',
            scope: 'write:orders',
          },
        },
      },
    ],
  },
});

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.

Custom token exchange

Exchange an external subject token for Auth0 tokens and establish an authenticated session using the token exchange flow (RFC 8693):

import { Component } from '@angular/core';
import { AuthService, CustomTokenExchangeOptions } from '@auth0/auth0-angular';

@Component({
  selector: 'app-token-exchange',
  template: `
    <button (click)="handleExchange()">Exchange Token</button>
    <div *ngIf="tokens">Token exchange successful!</div>
    <div *ngIf="error">Error: {{ error }}</div>
  `,
})
export class TokenExchangeComponent {
  tokens: any = null;
  error: string | null = null;

  constructor(private auth: AuthService) {}

  handleExchange() {
    const options: CustomTokenExchangeOptions = {
      subject_token: 'your-external-token',
      subject_token_type: 'urn:your-company:legacy-system-token',
      audience: 'https://api.example.com/',
      scope: 'openid profile email',
    };

    this.auth.loginWithCustomTokenExchange(options).subscribe({
      next: (tokenResponse) => {
        this.tokens = tokenResponse;
        this.error = null;

        // Use the returned tokens
        console.log('Access Token:', tokenResponse.access_token);
        console.log('ID Token:', tokenResponse.id_token);
      },
      error: (err) => {
        console.error('Token exchange failed:', err);
        this.error = err.message;
      },
    });
  }
}

Important Notes:

  • The subject_token_type must be a namespaced URI under your organization's control
  • The external token must be validated in Auth0 Actions using strong cryptographic verification
  • This method implements RFC 8693 token exchange grant type
  • The audience and scope can be provided directly in the options or will fall back to SDK defaults
  • State Management: This method updates the SDK's authentication state after a successful exchange, ensuring that isLoading$, isAuthenticated$, and user$ observables behave identically to the standard getAccessTokenSilently() flow

Wrapping the interceptor for granular control

While the allowedList configuration and uriMatcher provide flexible ways to control which requests receive access tokens, there may be scenarios where you need even more granular control on a per-request basis. For example:

  • Conditionally attaching tokens based on runtime state (not just URL patterns)
  • Using HttpContextTokens to bypass the interceptor for specific requests
  • Implementing environment-specific behavior (e.g., skip authentication in development)
  • Making the same request with and without a token based on user actions

In these cases, you can wrap the authHttpInterceptorFn in your own custom interceptor:

import { HttpRequest, HttpHandlerFn, HttpEvent, HttpContextToken } from '@angular/common/http';
import { authHttpInterceptorFn } from '@auth0/auth0-angular';
import { Observable } from 'rxjs';

export function customAuthInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
  // Implement shouldByPassAuth logic based on your needs
  if (shouldByPassAuth(req)) {
    return next(req);
  }

  // Otherwise, use the standard auth interceptor
  return authHttpInterceptorFn(req, next);
}

Register your custom interceptor instead of the built-in one:

import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAuth0 } from '@auth0/auth0-angular';

bootstrapApplication(AppComponent, {
  providers: [
    provideAuth0({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      authorizationParams: {
        audience: 'YOUR_AUTH0_API_IDENTIFIER',
      },
      httpInterceptor: {
        allowedList: ['/api/*'], // Configure as needed
      },
    }),
    provideHttpClient(withInterceptors([customAuthInterceptor])),
  ],
});

Important: allowedList still applies

When you wrap the interceptor, any request that you pass through to authHttpInterceptorFn must still match the allowedList configuration. If a request doesn't match the allowedList (or the configured glob patterns), the Auth0 interceptor will not add a token to that request, even if you intended it to.

This means:

  • If you bypass a request in your wrapper (e.g., using AUTH_INTERCEPTOR_BYPASS), it will never reach authHttpInterceptorFn, so the allowedList doesn't matter for that request.
  • If you pass a request through to authHttpInterceptorFn, ensure its URL matches your allowedList configuration, otherwise no token will be added.

Handling errors

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.

If you want to react to these errors, subscribe to the error$ observable and act accordingly.

ngOnInit() {
  this.authService.error$.subscribe(error => {
    // Handle Error here
  });
}

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.

ngOnInit() {
  this.authService.error$.pipe(
    filter((e) => e instanceof GenericError && e.error === 'login_required'),
    mergeMap(() => this.authService.loginWithRedirect())
  ).subscribe();
}

Organizations

Organizations is a set of features that provide better support for developers who build and maintain SaaS and Business-to-Business (B2B) applications.

Note that Organizations is currently only available to customers on our Enterprise and Startup subscription plans.

Log in to an organization

Log in to an organization by specifying the organization parameter importing the AuthModule:

AuthModule.forRoot({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  authorizationParams: {
    organization: 'YOUR_ORGANIZATION_ID_OR_NAME'
  }
}),

You can also specify the organization when logging in:

// Using a redirect
this.auth.loginWithRedirect({
  authorizationParams: {
    organization: 'YOUR_ORGANIZATION_ID_OR_NAME'
  }
});

// Using a popup window
this.auth.loginWithPopup({
  authorizationParams: {
    organization: 'YOUR_ORGANIZATION_ID_OR_NAME'
  }
});

Accept user invitations

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.

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  constructor(public auth: AuthService, private activatedRoute: ActivatedRoute) {}

  loginWithRedirect(): void {
    const { organization, invitation } = this.activatedRoute.snapshot.params;

    this.auth.loginWithRedirect({
      authorizationParams: {
        organization,
        invitation
      }
    });
  }
}

Device-bound tokens with DPoP

Demonstrating Proof-of-Possession —or simply DPoP— is a recent OAuth 2.0 extension defined in RFC9449.

It defines a mechanism for securely binding tokens to a specific device using cryptographic signatures. Without it, a token leak caused by XSS or other vulnerabilities could allow an attacker to impersonate the real user.

To support DPoP in auth0-angular, some APIs available in modern browsers are required:

The following OAuth 2.0 flows are currently supported by auth0-angular:

Important

Currently, only the ES256 algorithm is supported.

Enabling DPoP

DPoP is disabled by default. To enable it, set the useDpop option to true when configuring the SDK. For example:

import { NgModule } from '@angular/core';
import { AuthModule } from '@auth0/auth0-angular';

@NgModule({
  imports: [
    AuthModule.forRoot({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      useDpop: true, // 👈
      authorizationParams: {
        redirect_uri: window.location.origin,
      },
    }),
  ],
})
export class AppModule {}

After enabling DPoP, every new session using a supported OAuth 2.0 flow in Auth0 will begin transparently to use tokens that are cryptographically bound to the current browser.

Important

DPoP will only be used for new user sessions created after enabling it. Any previously existing sessions will continue using non-DPoP tokens until the user logs in again.

You decide how to handle this transition. For example, you might require users to log in again the next time they use your application.

Note

Using DPoP requires storing some temporary data in the user's browser. When you log the user out with logout(), this data is deleted.

Tip

If all your clients are already using DPoP, you may want to increase security by making Auth0 reject any non-DPoP interactions. See the docs on Sender Constraining for details.

Using DPoP in your own requests

You use a DPoP token the same way as a "traditional" access token, except it must be sent to the server with an Authorization: DPoP <token> header instead of the usual Authorization: Bearer <token>.

For internal requests sent by auth0-angular to Auth0, simply enable the useDpop option and every interaction with Auth0 will be protected.

However, to use DPoP with a custom, external API, some additional work is required. The AuthService provides some low-level methods to help with this:

  • getDpopNonce()
  • setDpopNonce()
  • generateDpopProof()

However, due to the nature of how DPoP works, this is not a trivial task:

  • When a nonce is missing or expired, the request may need to be retried.
  • Received nonces must be stored and managed.
  • DPoP headers must be generated and included in every request, and regenerated for retries.

Because of this, we recommend using the provided createFetcher() method with fetchWithAuth(), which handles all of this for you.

Simple usage

The fetchWithAuth() method is a drop-in replacement for the native fetch() function from the Fetch API, so if you're already using it, the change will be minimal.

For example, if you had this code:

const response = await fetch('https://api.example.com/foo', {
  method: 'GET',
  headers: { 'user-agent': 'My Client 1.0' },
});

console.log(response.status);
console.log(response.headers);
console.log(await response.json());

You would change it as follows:

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({
  selector: 'app-data',
  template: `...`,
})
export class DataComponent {
  constructor(private auth: AuthService) {}

  async fetchData() {
    const fetcher = this.auth.createFetcher({
      dpopNonceId: 'my_api_request',
    });

    const response = await fetcher.fetchWithAuth('https://api.example.com/foo', {
      method: 'GET',
      headers: { 'user-agent': 'My Client 1.0' },
    });

    console.log(response.status);
    console.log(response.headers);
    console.log(await response.json());
  }
}

When using fetchWithAuth(), the following will be handled for you automatically:

  • Use getAccessTokenSilently() to get the access token to inject in the headers.
  • Generate and inject DPoP headers when needed.
  • Store and update any DPoP nonces.
  • Handle retries caused by a rejected nonce.

Important

If DPoP is enabled, a dpopNonceId must be present in the createFetcher() parameters, since it's used to keep track of the DPoP nonces for each request.

Advanced usage

If you need something more complex than the example above, you can provide a custom implementation in the fetch property.

However, since auth0-angular needs to make decisions based on HTTP responses, your implementation must return an object with at least two properties:

  1. status: the response status code as a number.
  2. headers: the response headers as a plain object or as a Fetch API's Headers-like interface.

Whatever it returns, it will be passed as the output of the fetchWithAuth() method.

Your implementation will be called with a standard, ready-to-use Request object, which will contain any headers needed for authorization and DPoP usage (if enabled). Depending on your needs, you can use this object directly or treat it as a container with everything required to make the request your own way.

Having a base URL

If you need to make requests to different endpoints of the same API, passing a baseUrl to createFetcher() can be useful:

import { Injectable } from '@angular/core';
import { AuthService, Fetcher } from '@auth0/auth0-angular';

@Injectable({ providedIn: 'root' })
export class ApiService {
  private fetcher: Fetcher;

  constructor(private auth: AuthService) {
    this.fetcher = this.auth.createFetcher({
      dpopNonceId: 'my-api',
      baseUrl: 'https://api.example.com',
    });
  }

  async getFoo() {
    return this.fetcher.fetchWithAuth('/foo'); // => https://api.example.com/foo
  }

  async getBar() {
    return this.fetcher.fetchWithAuth('/bar'); // => https://api.example.com/bar
  }

  async getXyz() {
    return this.fetcher.fetchWithAuth('/xyz'); // => https://api.example.com/xyz
  }

  async getFromOtherApi() {
    // If the passed URL is absolute, `baseUrl` will be ignored for convenience:
    return this.fetcher.fetchWithAuth('https://other-api.example.com/foo');
  }
}
Multiple API endpoints

When working with multiple APIs, create separate fetchers for each. Each fetcher manages its own nonces independently:

import { Injectable } from '@angular/core';
import { AuthService, Fetcher } from '@auth0/auth0-angular';

@Injectable({ providedIn: 'root' })
export class MultiApiService {
  private internalApi: Fetcher;
  private partnerApi: Fetcher;

  constructor(private auth: AuthService) {
    // Each fetcher manages its own nonces independently
    this.internalApi = this.auth.createFetcher({
      dpopNonceId: 'internal-api',
      baseUrl: 'https://internal.example.com',
    });

    this.partnerApi = this.auth.createFetcher({
      dpopNonceId: 'partner-api',
      baseUrl: 'https://partner.example.com',
    });
  }

  async getInternalData() {
    const response = await this.internalApi.fetchWithAuth('/data');
    return response.json();
  }

  async getPartnerResources() {
    const response = await this.partnerApi.fetchWithAuth('/resources');
    return response.json();
  }

  async getAllData() {
    const [internal, partner] = await Promise.all([this.getInternalData(), this.getPartnerResources()]);
    return { internal, partner };
  }
}
Manual DPoP management

For scenarios requiring full control over DPoP proof generation and nonce management, you can use the low-level methods:

import { Component } from '@angular/core';
import { AuthService, UseDpopNonceError } from '@auth0/auth0-angular';
import { firstValueFrom } from 'rxjs';

@Component({
  selector: 'app-advanced',
  template: `<button (click)="makeRequest()">Make Request</button>`,
})
export class AdvancedComponent {
  constructor(private auth: AuthService) {}

  async makeRequest() {
    try {
      // 1. Get access token
      const token = await firstValueFrom(this.auth.getAccessTokenSilently());

      // 2. Get current DPoP nonce for the API
      const nonce = await firstValueFrom(this.auth.getDpopNonce('my-api'));

      // 3. Generate DPoP proof
      const proof = await firstValueFrom(
        this.auth.generateDpopProof({
          url: 'https://api.example.com/data',
          method: 'POST',
          accessToken: token!,
          nonce,
        })
      );

      // 4. Make the API request
      const response = await fetch('https://api.example.com/data', {
        method: 'POST',
        headers: {
          Authorization: `DPoP ${token}`,
          DPoP: proof!,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ data: 'example' }),
      });

      // 5. Update nonce if server provides a new one
      const newNonce = response.headers.get('DPoP-Nonce');
      if (newNonce) {
        await firstValueFrom(this.auth.setDpopNonce(newNonce, 'my-api'));
      }

      const data = await response.json();
      console.log('Success:', data);
    } catch (error) {
      if (error instanceof UseDpopNonceError) {
        console.error('DPoP nonce error:', error.message);
      } else {
        console.error('Request failed:', error);
      }
    }
  }
}

Standalone Components with DPoP

When using standalone components, enable DPoP in your provideAuth0 configuration:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideAuth0 } from '@auth0/auth0-angular';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideAuth0({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      useDpop: true, // 👈
      authorizationParams: {
        redirect_uri: window.location.origin,
      },
    }),
  ],
});

Online Access (Online Refresh Tokens)

Note

Online Access (Online Refresh Tokens) support via SDKs is currently in Early Access. To request access to this feature, contact your Auth0 representative.

Online Refresh Tokens (ORTs) are a refresh token type bound to the lifetime of the user's Auth0 session, unlike rotating offline refresh tokens. An ORT is:

  • Session-bound — valid only while the underlying Auth0 session is active. When the session ends (logout, idle/absolute session expiry, or an admin revoking the session), the ORT stops working.
  • Non-rotating — refreshing an access token with an ORT does not issue a new refresh token; the same ORT is reused for the life of the session.

Read more about Online Refresh Tokens to decide whether this fits your application.

Important

Online access requires DPoP. Sender-constraining the token via DPoP is mandatory because the ORT is non-rotating — binding it to the browser's key pair is what mitigates token replay if it is exfiltrated. You must set useDpop: true explicitly; the SDK does not enable it for you.

This also requires allow_online_access to be enabled on the resource server you log in with.

Warning

Online Refresh Tokens do not currently support resource servers with Ephemeral Sessions enabled. If a resource server has both allow_online_access and "Allow for Ephemeral Sessions" enabled, the authorization server issues an Online Refresh Token at login that is then rejected with invalid_grant ("Unknown or invalid refresh token") on the very next refresh — this is a known backend limitation, not a client-side defect. Until Ephemeral Sessions support is added for Online Refresh Tokens, disable "Allow for Ephemeral Sessions" on any resource server used with refreshTokenMode: RefreshTokenMode.Online.

Enabling Online Access

Set refreshTokenMode to RefreshTokenMode.Online together with useRefreshTokens: true and useDpop: true:

import { NgModule } from '@angular/core';
import { AuthModule, RefreshTokenMode } from '@auth0/auth0-angular';

@NgModule({
  imports: [
    AuthModule.forRoot({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      useRefreshTokens: true, // required — online access is a refresh-token grant
      refreshTokenMode: RefreshTokenMode.Online, // 👈
      useDpop: true, // required — DPoP is mandatory for online access
      authorizationParams: {
        redirect_uri: window.location.origin,
      },
    }),
  ],
})
export class AppModule {}

Or with standalone components via provideAuth0:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideAuth0, RefreshTokenMode } from '@auth0/auth0-angular';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [
    provideAuth0({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      useRefreshTokens: true,
      refreshTokenMode: RefreshTokenMode.Online,
      useDpop: true,
      authorizationParams: {
        redirect_uri: window.location.origin,
      },
    }),
  ],
});

refreshTokenMode defaults to RefreshTokenMode.Offline (rotating refresh tokens). Enabling online mode causes the underlying SDK to:

  • Send the online_access scope to the authorization server (instead of offline_access) — you do not need to add it to authorizationParams.scope yourself.
  • Route token renewal through the refresh_token grant against /oauth/token rather than a hidden iframe.
  • Store the non-rotating ORT in the existing cache and reuse it on every refresh, never replacing it.

Configuration validation

If refreshTokenMode: RefreshTokenMode.Online is set without useRefreshTokens: true and useDpop: true, the underlying Auth0Client constructor throws an InvalidConfigurationError. Auth0Client is constructed lazily by Angular's DI system, so the error surfaces during app bootstrap rather than at the AuthModule.forRoot / provideAuth0 call site — a synchronous try/catch around that call won't catch it.

Catch it on the bootstrap promise:

import { InvalidConfigurationError } from '@auth0/auth0-angular';

bootstrapApplication(AppComponent, {
  providers: [
    provideAuth0({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      useRefreshTokens: true,
      refreshTokenMode: RefreshTokenMode.Online,
      useDpop: true,
      authorizationParams: { redirect_uri: window.location.origin },
    }),
  ],
}).catch((e) => {
  if (e instanceof InvalidConfigurationError) {
    console.error('Auth0 misconfiguration:', e.error_description); // includes the suggested fix
  }
});

Or handle it globally with Angular's ErrorHandler, which also receives errors from the running app:

import { ErrorHandler, Injectable } from '@angular/core';
import { InvalidConfigurationError } from '@auth0/auth0-angular';

@Injectable()
export class AppErrorHandler implements ErrorHandler {
  handleError(error: unknown): void {
    if (error instanceof InvalidConfigurationError) {
      console.error('Auth0 misconfiguration:', error.error_description);
      return;
    }
    console.error(error);
  }
}

// in app.config.ts:
{ provide: ErrorHandler, useClass: AppErrorHandler }

Using Online Access with MRRT

Online access is compatible with Multi-Resource Refresh Tokens (MRRT): a single ORT can be exchanged for access tokens across the audiences allowed by your refresh-token policies. The ORT remains non-rotating throughout.

AuthModule.forRoot({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  useRefreshTokens: true,
  refreshTokenMode: RefreshTokenMode.Online,
  useDpop: true,
  useMrrt: true, // 👈
  authorizationParams: {
    redirect_uri: window.location.origin,
    audience: 'https://api.example.com',
  },
});

Important

In order for MRRT to work, it needs a previous configuration setting the refresh token policies. Visit configure and implement MRRT.

Standalone components and a more functional approach

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.

There are a couple of difference with how you would traditionally implement our SDK:

  • Use our functional guard (authGuardFn) instead of our class-based AuthGuard.
  • Use our functional interceptor (authHttpInterceptorFn) instead of our class-based AuthHttpInterceptor.
  • Register the interceptor by passing it to withInterceptors when calling provideHttpClient.
  • Register our SDK using provideAuth0.
import { authGuardFn, authHttpInterceptorFn, provideAuth0 } from '@auth0/auth0-angular';

const routes: Routes = [
  {
    path: 'profile',
    component: ProfileComponent,
    canActivate: [authGuardFn],
  },
];

bootstrapApplication(AppComponent, {
  providers: [provideRouter(routes), provideAuth0(/* Auth Config Goes Here */), provideHttpClient(withInterceptors([authHttpInterceptorFn]))],
});

Important: provideAuth0 returns EnvironmentProviders, which ensures it can only be used at the application/environment level. Attempting to add it to a component's providers array will result in a compile-time error.

Connect Accounts for using Token Vault

The Connect Accounts feature uses the Auth0 My Account API to allow users to link multiple third party accounts to a single Auth0 user profile.

When using Connected Accounts, Auth0 acquires tokens from upstream Identity Providers (like Google) and stores them in a secure Token Vault. These tokens can then be used to access third-party APIs (like Google Calendar) on behalf of the user.

The tokens in the Token Vault are then accessible to Resource Servers (APIs) configured in Auth0. The SPA application can then issue requests to the API, which can retrieve the tokens from the Token Vault and use them to access the third-party APIs.

This is particularly useful for applications that require access to different resources on behalf of a user, like AI Agents.

Configure the SDK

The SDK must be configured with an audience (an API Identifier) - this will be the resource server that uses the tokens from the Token Vault.

The SDK must also be configured to use refresh tokens and MRRT (Multiple Resource Refresh Tokens) since we will use the refresh token grant to get Access Tokens for the My Account API in addition to the API we are calling.

The My Account API requires DPoP tokens, so we also need to enable DPoP.

AuthModule.forRoot({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  useRefreshTokens: true,
  useMrrt: true,
  useDpop: true,
  authorizationParams: {
    redirect_uri: window.location.origin,
  },
});

Login to the application

Use the login methods to authenticate to the application and get a refresh and access token for the API.

// Login specifying any scopes for the Auth0 API
this.auth
  .loginWithRedirect({
    authorizationParams: {
      audience: 'YOUR_AUTH0_API_IDENTIFIER',
      scope: 'openid profile email read:calendar',
    },
  })
  .subscribe();

Connect to a third party account

Use the connectAccountWithRedirect method to redirect the user to the third party Identity Provider to connect their account.

// Start the connect flow by redirecting to the third party API's login, defined as an Auth0 connection
this.auth
  .connectAccountWithRedirect({
    connection: '<CONNECTION eg, google-apps-connection>',
    scopes: ['<SCOPE eg https://www.googleapis.com/auth/calendar.acls.readonly>'],
    authorizationParams: {
      // additional authorization params to forward to the authorization server
    },
  })
  .subscribe();

When the redirect completes, the user will be returned to the application and the tokens from the third party Identity Provider will be stored in the Token Vault. You can access the connected account details via the appState$ observable:

ngOnInit() {
  this.auth.appState$.subscribe((appState) => {
    if (appState?.connectedAccount) {
      console.log(`You've connected to ${appState.connectedAccount.connection}`);
      // Handle the connected account details
      // appState.connectedAccount contains: id, connection, access_type, created_at, expires_at
    }
  });
}

List connected accounts

To retrieve the accounts a user has connected, get an access token for the My Account API and call the /v1/connected-accounts/accounts endpoint:

this.auth
  .getAccessTokenSilently({
    authorizationParams: {
      audience: `https://YOUR_AUTH0_DOMAIN/me/`,
      scope: 'read:me:connected_accounts',
    },
  })
  .subscribe(async (token) => {
    const res = await fetch(`https://YOUR_AUTH0_DOMAIN/me/v1/connected-accounts/accounts`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    const { accounts } = await res.json();
    // accounts contains: id, connection, access_type, scopes, created_at
  });

You can now call the API with your access token and the API can use Access Token Exchange with Token Vault to get tokens from the Token Vault to access third party APIs on behalf of the user.

Important

You must enable Offline Access from the Connection Permissions settings to be able to use the connection with Connected Accounts.

Native to Web SSO

Native to Web SSO enables seamless single sign-on when users transition from a native mobile app to a web app. The SDK can automatically extract a session transfer token from the URL and include it in the authorization request.

The feature is disabled by default. To enable it, set sessionTransferTokenQueryParamName in the SDK configuration with the name of the query parameter your native app appends to the web app URL:

AuthModule.forRoot({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  authorizationParams: {
    redirect_uri: window.location.origin,
  },
  sessionTransferTokenQueryParamName: 'session_transfer_token',
}),

Or using provideAuth0:

provideAuth0({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  authorizationParams: {
    redirect_uri: window.location.origin,
  },
  sessionTransferTokenQueryParamName: 'session_transfer_token',
}),

When the web app is opened with ?session_transfer_token=xyz in the URL, the SDK extracts the token, includes it in the /authorize request, and removes it from the URL via window.history.replaceState().

Using a custom parameter name

If your native app uses a different query parameter name, configure that name instead. The token is always forwarded to Auth0 as session_transfer_token regardless:

AuthModule.forRoot({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  authorizationParams: {
    redirect_uri: window.location.origin,
  },
  sessionTransferTokenQueryParamName: 'stt',
}),

Manually providing the session transfer token

You can pass the token directly via authorizationParams. This takes precedence over automatic URL detection:

this.auth.loginWithRedirect({
  authorizationParams: {
    session_transfer_token: 'YOUR_SESSION_TRANSFER_TOKEN',
  },
});

Multi-Factor Authentication (MFA)

Access MFA operations through the mfa property on AuthService. All operations require an mfa_token from the MfaRequiredError thrown by getAccessTokenSilently.

Note

Multi Factor Authentication support via SDKs is currently in Early Access. To request access to this feature, contact your Auth0 representative.

MFA Setup

Before using the MFA API, configure MFA in your Auth0 Dashboard under Security > Multi-factor Auth. For detailed configuration, see the Auth0 MFA documentation.

Understanding the MFA Response

When MFA is required, the error payload contains an mfa_requirements object that indicates either a challenge flow (user has enrolled authenticators) or an enroll flow (user needs to set up MFA).

Challenge Flow Response (user has existing authenticators):

{
  "error": "mfa_required",
  "error_description": "Multifactor authentication required",
  "mfa_token": "Fe26.2*...",
  "mfa_requirements": {
    "challenge": [{ "type": "otp" }, { "type": "email" }]
  }
}

Enroll Flow Response (user needs to enroll an authenticator):

{
  "error": "mfa_required",
  "error_description": "Multifactor authentication required",
  "mfa_token": "Fe26.2*...",
  "mfa_requirements": {
    "enroll": [{ "type": "otp" }, { "type": "phone" }, { "type": "push-notification" }]
  }
}

These two keys are mutually exclusive — a single response will contain either challenge or enroll, never both:

  • mfa_requirements.challenge: User has enrolled authenticators → proceed with List Authenticators → Challenge → Verify flow
  • mfa_requirements.enroll: User needs to set up MFA → proceed with Enroll → Verify flow

Handling MFA Required Error

Catch the MfaRequiredError from getAccessTokenSilently and use mfa_requirements to determine which flow to follow:

import { Component } from '@angular/core';
import { AuthService, MfaRequiredError } from '@auth0/auth0-angular';
import { catchError, EMPTY, tap } from 'rxjs';

@Component({ selector: 'app-mfa', template: '' })
export class MfaComponent {
  constructor(private auth: AuthService) {}

  requestToken() {
    this.auth
      .getAccessTokenSilently()
      .pipe(
        catchError((error) => {
          if (error instanceof MfaRequiredError) {
            const mfaToken = error.mfa_token;

            if (error.mfa_requirements?.enroll?.length) {
              // New user — needs to enroll a factor first
              return this.auth.mfa.getEnrollmentFactors(mfaToken).pipe(
                tap((factors) => {
                  // Show enrollment UI with available factors
                })
              );
            } else {
              // Existing user — list enrolled authenticators and challenge
              return this.auth.mfa.getAuthenticators(mfaToken).pipe(
                tap((authenticators) => {
                  // Show challenge UI
                })
              );
            }
          }
          return EMPTY;
        })
      )
      .subscribe();
  }
}

Enrolling Authenticators

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({ selector: 'app-enroll', template: '' })
export class EnrollComponent {
  constructor(private auth: AuthService) {}

  // Enroll TOTP — returns a QR code to display to the user
  enrollOtp(mfaToken: string) {
    this.auth.mfa.enroll({ mfaToken, factorType: 'otp' }).subscribe((enrollment) => {
      console.log('Scan QR:', enrollment.barcodeUri);
      console.log('Recovery codes:', enrollment.recoveryCodes);
    });
  }

  // Enroll SMS — include phone number in E.164 format
  enrollSms(mfaToken: string) {
    this.auth.mfa
      .enroll({
        mfaToken,
        factorType: 'sms',
        phoneNumber: '+12025551234',
      })
      .subscribe();
  }

  // Enroll Voice — include phone number in E.164 format
  enrollVoice(mfaToken: string) {
    this.auth.mfa
      .enroll({
        mfaToken,
        factorType: 'voice',
        phoneNumber: '+12025551234',
      })
      .subscribe();
  }

  // Enroll Email
  enrollEmail(mfaToken: string) {
    this.auth.mfa
      .enroll({
        mfaToken,
        factorType: 'email',
        email: 'user@example.com',
      })
      .subscribe();
  }

  // Enroll Push — returns authenticator ID for use with the Guardian app
  enrollPush(mfaToken: string) {
    this.auth.mfa.enroll({ mfaToken, factorType: 'push' }).subscribe((enrollment) => {
      console.log('Authenticator ID:', enrollment.id);
    });
  }
}

Challenging Authenticators

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';
import { switchMap } from 'rxjs';

@Component({ selector: 'app-challenge', template: '' })
export class ChallengeComponent {
  constructor(private auth: AuthService) {}

  // For OTP: challenge is optional — user can go straight to verify()
  // with the 6-digit code from their authenticator app
  challengeOtp(mfaToken: string, authenticatorId: string) {
    this.auth.mfa
      .challenge({
        mfaToken,
        challengeType: 'otp',
        authenticatorId,
      })
      .subscribe();
  }

  // For SMS / Voice / Email / Push: challenge is required to send the code
  challengeOob(mfaToken: string, authenticatorId: string) {
    this.auth.mfa
      .challenge({
        mfaToken,
        challengeType: 'oob',
        authenticatorId,
      })
      .subscribe((response) => {
        console.log('OOB Code:', response.oobCode); // use this in verify()
      });
  }

  // Typical flow: list authenticators then challenge
  listAndChallenge(mfaToken: string) {
    this.auth.mfa
      .getAuthenticators(mfaToken)
      .pipe(
        switchMap((authenticators) =>
          this.auth.mfa.challenge({
            mfaToken,
            challengeType: 'oob',
            authenticatorId: authenticators[0].id,
          })
        )
      )
      .subscribe((response) => {
        // Code has been sent — show input to user
      });
  }
}

Verifying Challenges

Important

The verify() method does not update Angular auth state (isAuthenticated$, user$). Always chain getAccessTokenSilently() after a successful verification to reflect the new session in the UI.

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';
import { switchMap, tap } from 'rxjs';

@Component({ selector: 'app-verify', template: '' })
export class VerifyComponent {
  constructor(private auth: AuthService) {}

  // Verify with OTP code (TOTP authenticator app)
  verifyOtp(mfaToken: string, otp: string) {
    this.auth.mfa
      .verify({ mfaToken, otp })
      .pipe(
        switchMap(() => this.auth.getAccessTokenSilently()) // refresh isAuthenticated$, user$
      )
      .subscribe();
  }

  // Verify with OOB code (SMS / Voice / Email / Push)
  verifyOob(mfaToken: string, oobCode: string, bindingCode?: string) {
    this.auth.mfa
      .verify({ mfaToken, oobCode, bindingCode })
      .pipe(
        switchMap(() => this.auth.getAccessTokenSilently()) // refresh isAuthenticated$, user$
      )
      .subscribe();
  }

  // Verify with recovery code (fallback for any authenticator)
  // When a recovery code is consumed, Auth0 may return a replacement recovery_code
  // in the response. Prompt the user to save it — losing the new code locks them out.
  verifyRecoveryCode(mfaToken: string, recoveryCode: string) {
    this.auth.mfa
      .verify({ mfaToken, recoveryCode })
      .pipe(
        tap((tokens) => {
          if (tokens.recovery_code) {
            console.warn('Save your new recovery code:', tokens.recovery_code);
          }
        }),
        switchMap(() => this.auth.getAccessTokenSilently()) // refresh isAuthenticated$, user$
      )
      .subscribe();
  }
}

MFA Error Handling

Each MFA operation throws a specific error class you can import from @auth0/auth0-angular:

import { MfaVerifyError, MfaChallengeError, MfaEnrollmentError, MfaListAuthenticatorsError, MfaEnrollmentFactorsError } from '@auth0/auth0-angular';
import { catchError, EMPTY } from 'rxjs';

this.auth.mfa
  .verify({ mfaToken, otp })
  .pipe(
    catchError((error) => {
      if (error instanceof MfaVerifyError) {
        console.error('Invalid code:', error.error_description);
      } else if (error instanceof MfaChallengeError) {
        console.error('Challenge failed:', error.error_description);
      } else if (error instanceof MfaEnrollmentError) {
        console.error('Enrollment failed:', error.error_description);
      }
      return EMPTY;
    })
  )
  .subscribe();

Step-Up Authentication

When a protected API requires MFA, getAccessTokenSilently receives an mfa_required error from Auth0. By configuring interactiveErrorHandler, the SDK automatically handles this by opening a Universal Login popup for the user to complete MFA, then returns the token transparently. No custom MFA UI is required.

If you need full control over the MFA experience (custom UI for enrollment, challenge, and verification), see the Multi-Factor Authentication (MFA) section instead.

Warning

This feature only works with the refresh token flow (useRefreshTokens: true) and only handles mfa_required errors.

Step-Up Setup

Configure provideAuth0 (or AuthModule.forRoot) with interactiveErrorHandler set to "popup" and refresh tokens enabled:

// app.config.ts — standalone / functional approach
import { provideAuth0 } from '@auth0/auth0-angular';

export const appConfig = {
  providers: [
    provideAuth0({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      authorizationParams: {
        redirect_uri: window.location.origin,
        audience: 'https://api.example.com/',
      },
      useRefreshTokens: true,
      interactiveErrorHandler: 'popup',
    }),
  ],
};
// app.module.ts — NgModule approach
import { AuthModule } from '@auth0/auth0-angular';

@NgModule({
  imports: [
    AuthModule.forRoot({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      authorizationParams: {
        redirect_uri: window.location.origin,
        audience: 'https://api.example.com/',
      },
      useRefreshTokens: true,
      interactiveErrorHandler: 'popup',
    }),
  ],
})
export class AppModule {}

Usage

With this configuration, getAccessTokenSilently automatically opens a popup when the token request triggers an mfa_required error. Once the user completes MFA in the popup, the token is returned as if the call succeeded normally:

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({ selector: 'app-protected', template: '' })
export class ProtectedComponent {
  constructor(private auth: AuthService) {}

  fetchSensitiveData() {
    this.auth
      .getAccessTokenSilently({
        authorizationParams: {
          audience: 'https://api.example.com/',
          scope: 'read:sensitive',
        },
      })
      .subscribe({
        next: (token) => {
          // If MFA was required, the popup opened and closed automatically.
          // token is ready to use.
          fetch('https://api.example.com/sensitive', {
            headers: { Authorization: `Bearer ${token}` },
          });
        },
        error: (e) => console.error(e),
      });
  }
}

Error Handling

If the popup is blocked, cancelled, or times out, getAccessTokenSilently throws PopupOpenError, PopupCancelledError, or PopupTimeoutError respectively. These can be imported from @auth0/auth0-angular:

import { PopupOpenError, PopupCancelledError, PopupTimeoutError } from '@auth0/auth0-angular';
import { catchError, EMPTY } from 'rxjs';

this.auth
  .getAccessTokenSilently({
    authorizationParams: { audience: 'https://api.example.com/' },
  })
  .pipe(
    catchError((error) => {
      if (error instanceof PopupOpenError) {
        console.error('Popup was blocked by the browser');
      } else if (error instanceof PopupCancelledError) {
        console.error('User closed the popup');
      } else if (error instanceof PopupTimeoutError) {
        console.error('Popup timed out');
      }
      return EMPTY;
    })
  )
  .subscribe();

Passkeys

Passkeys provide passwordless authentication using platform biometrics (Face ID, Touch ID, Windows Hello) or security keys via the WebAuthn standard. AuthService exposes a passkey client that handles the full WebAuthn flow internally — requesting a challenge, triggering the browser credential ceremony, and exchanging the result for tokens.

After a successful call, isAuthenticated$ and user$ update automatically.

Passkey Setup

Before using passkeys, configure the following in your Auth0 Dashboard:

  1. Enable passkey authentication method: Go to Authentication > Database > your connection > Authentication Methods > Passkey.
  2. Enable the WebAuthn passkey grant: Go to your Application > Advanced Settings > Grant Types and enable the Passkey grant.
  3. Custom domain required: Passkeys are bound to an origin. A custom domain must be configured — passkeys will not work on the default *.auth0.com domain.

Important: Use Refresh Tokens with Passkeys

Important

When using passkeys you must configure the SDK with useRefreshTokens: true.

Passkey authentication performs a direct token exchange and does not create an Auth0 session cookie. Without refresh tokens, getAccessTokenSilently() will either fail with login_required when the token expires, or silently return tokens for a different user if a prior redirect-based session cookie exists.

// app.config.ts (standalone)
import { provideAuth0 } from '@auth0/auth0-angular';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAuth0({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      useRefreshTokens: true, // Required for passkey-based sessions
      authorizationParams: {
        redirect_uri: window.location.origin,
      },
    }),
  ],
};

It is also recommended to enable Refresh Token Rotation in your Auth0 Dashboard under Applications > your app > Settings > Refresh Token Rotation.

Signup with Passkey

Register a new user with a passkey. Inject AuthService and call passkey.signup(). The Observable completes with the token response, and isAuthenticated$ / user$ update automatically.

import { Component, inject } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({ selector: 'app-signup', template: `<button (click)="signup()">Sign up with Passkey</button>` })
export class SignupComponent {
  private auth = inject(AuthService);

  signup() {
    this.auth.passkey
      .signup({ email: 'user@example.com', name: 'Jane Doe' })
      .subscribe({
        next: (tokens) => console.log('Signed up, access token:', tokens.access_token),
        error: (err) => console.error('Signup failed', err),
      });
  }
}

You can pass optional scope and audience to control the access token:

this.auth.passkey
  .signup({
    email: 'user@example.com',
    scope: 'openid profile email read:orders',
    audience: 'https://api.example.com',
  })
  .subscribe();

All supported signup properties:

this.auth.passkey
  .signup({
    // At least one identifier is typically required
    email: 'user@example.com',
    phoneNumber: '+1234567890', // optional, E.164 format
    username: 'janedoe',        // optional

    // Profile fields (all optional)
    name: 'Jane Doe',
    givenName: 'Jane',
    familyName: 'Doe',
    nickname: 'janie',
    picture: 'https://example.com/avatar.png',
    userMetadata: { plan: 'pro' },

    // Connection and org
    realm: 'my-db-connection',
    organization: 'org_abc123',

    // Token options
    scope: 'openid profile email',
    audience: 'https://api.example.com',
  })
  .subscribe();

Login with Passkey

Authenticate an existing user with their registered passkey. A single call handles the full assertion flow.

import { Component, inject } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({ selector: 'app-login', template: `<button (click)="login()">Sign in with Passkey</button>` })
export class LoginComponent {
  private auth = inject(AuthService);

  login() {
    this.auth.passkey.login().subscribe({
      next: () => console.log('Logged in'),
      error: (err) => console.error('Login failed', err),
    });
  }
}

Optional parameters:

// Specify a connection (realm) if your tenant has multiple database connections
this.auth.passkey.login({ realm: 'Username-Password-Authentication' }).subscribe();

// Log in within an organization context
this.auth.passkey.login({ organization: 'org_abc123' }).subscribe();

Complete Passkey Component Example

import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { AuthService, PasskeyError, PasskeyRegisterError } from '@auth0/auth0-angular';

@Component({
  selector: 'app-passkey-auth',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <ng-container *ngIf="auth.isAuthenticated$ | async; else unauthenticated">
      <p>Welcome, {{ (auth.user$ | async)?.name }}!</p>
    </ng-container>
    <ng-template #unauthenticated>
      <button (click)="signup()">Sign up with Passkey</button>
      <button (click)="login()">Sign in with Passkey</button>
    </ng-template>
  `,
})
export class PasskeyAuthComponent {
  auth = inject(AuthService);

  signup() {
    this.auth.passkey
      .signup({ email: 'user@example.com' })
      .subscribe({
        error: (err) => {
          if (err instanceof PasskeyRegisterError) {
            console.error('Registration failed:', err.message);
          } else if (err instanceof PasskeyError) {
            console.error('Passkey error:', err.message);
          }
        },
      });
  }

  login() {
    this.auth.passkey.login().subscribe({
      error: (err) => {
        if (err instanceof PasskeyError) {
          console.error('Passkey error:', err.message);
        }
      },
    });
  }
}

Passkey Error Handling

import { PasskeyError, PasskeyRegisterError, PasskeyChallengeError } from '@auth0/auth0-angular';
import { catchError, EMPTY } from 'rxjs';

// In a component or service
this.auth.passkey
  .signup({ email: 'user@example.com' })
  .pipe(
    catchError((err) => {
      if (err instanceof PasskeyRegisterError) {
        // WebAuthn registration failed (e.g. user cancelled the biometric prompt)
        console.error('Registration failed:', err.message);
      } else if (err instanceof PasskeyError) {
        // Auth0 returned an error (e.g. misconfigured grant, unknown user)
        console.error('Passkey error:', err.code, err.message);
      }
      return EMPTY;
    })
  )
  .subscribe();

this.auth.passkey
  .login()
  .pipe(
    catchError((err) => {
      if (err instanceof PasskeyChallengeError) {
        console.error('Challenge failed:', err.message);
      } else if (err instanceof PasskeyError) {
        console.error('Passkey error:', err.code, err.message);
      }
      return EMPTY;
    })
  )
  .subscribe();

Tip

Both signup() and login() throw an error if the user cancels the biometric prompt. Always handle errors to avoid unhandled Observable errors crashing your application.

If your tenant requires MFA after a passkey login, passkey.login() will throw an MfaRequiredError. Handle it using the mfa client on AuthService:

import { AuthService, PasskeyError, MfaRequiredError } from '@auth0/auth0-angular';
import { catchError, EMPTY } from 'rxjs';

// In a component or service
this.auth.passkey
  .login()
  .pipe(
    catchError((err) => {
      if (err instanceof MfaRequiredError) {
        // MFA step-up required — proceed with the MFA API
        const mfaToken = err.mfa_token;
        this.auth.mfa.getAuthenticators(mfaToken).subscribe((authenticators) => {
          // present authenticator picker and continue with challenge/verify
          // see the Multi-Factor Authentication section for full examples
        });
      } else if (err instanceof PasskeyError) {
        console.error('Passkey login failed:', err.message);
      }
      return EMPTY;
    })
  )
  .subscribe();

MyAccount API

The MyAccount API lets authenticated users manage their own authentication methods (passkeys, TOTP, phone, email, push, recovery codes, passwords) and list available factors — all without requiring an admin token.

AuthService exposes a myAccount client. All methods return Observables and require an access token with the appropriate scope for each operation. Use MRRT to obtain tokens for the MyAccount API alongside your own API audience.

MyAccount Setup

The MyAccount API uses the https://<YOUR_DOMAIN>/me/ audience. Configure the SDK with useRefreshTokens: true and useMrrt: true so the SDK can automatically obtain a MyAccount-scoped token using the refresh token grant, even when your app is already configured with a different API audience.

// app.config.ts
import { provideAuth0 } from '@auth0/auth0-angular';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAuth0({
      domain: 'YOUR_AUTH0_DOMAIN',
      clientId: 'YOUR_AUTH0_CLIENT_ID',
      useRefreshTokens: true,
      useMrrt: true,
      authorizationParams: {
        redirect_uri: window.location.origin,
        audience: 'https://api.example.com', // your own API — MyAccount tokens are fetched automatically via MRRT
      },
    }),
  ],
};

Also ensure the required permissions are enabled for your application in the Auth0 Dashboard under APIs > Auth0 My Account API > Application Access.

List Factors

Get the list of MFA factors and their enabled/enrollment status for the current user.

import { Component, inject } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({ selector: 'app-factors', template: '' })
export class FactorsComponent {
  private auth = inject(AuthService);

  loadFactors() {
    this.auth.myAccount.getFactors().subscribe({
      next: (factors) => console.log(factors),
      // [{ type: 'totp', usage: ['secondary'] }, { type: 'phone', usage: ['secondary'] }]
      error: (err) => console.error(err),
    });
  }
}

Authentication Methods

List All

this.auth.myAccount.getAuthenticationMethods().subscribe({
  next: (methods) => console.log(methods),
  error: (err) => console.error(err),
});

Filter by Type

import { AuthenticationMethodType } from '@auth0/auth0-angular';

this.auth.myAccount.getAuthenticationMethods('passkey').subscribe({
  next: (passkeys) => console.log(passkeys),
});

Supported type values: 'passkey', 'password', 'phone', 'totp', 'email', 'push-notification', 'recovery-code', 'webauthn-platform', 'webauthn-roaming'.

Get by ID

this.auth.myAccount.getAuthenticationMethod('am_abc123').subscribe({
  next: (method) => console.log(method),
});

Delete

this.auth.myAccount.deleteAuthenticationMethod('am_abc123').subscribe({
  next: () => console.log('Deleted'),
  error: (err) => console.error(err),
});

Update

Rename a totp or push-notification method, or change the preferred delivery channel for a phone method. Note that renaming is not supported for passkey methods.

// Rename a totp or push-notification method
this.auth.myAccount
  .updateAuthenticationMethod('am_abc123', { name: 'My Work Laptop' })
  .subscribe({ next: (updated) => console.log(updated) });

// Switch a phone method between SMS and voice
this.auth.myAccount
  .updateAuthenticationMethod('am_abc123', { preferred_authentication_method: 'voice' })
  .subscribe({ next: (updated) => console.log(updated) });

Enrollment

Enrollment is a two-step flow: call enrollmentChallenge() to start, then enrollmentVerify() to complete. Use RxJS switchMap to chain the two steps cleanly.

Enroll Passkey

import { switchMap } from 'rxjs';

this.auth.myAccount
  .enrollmentChallenge({ type: 'passkey' })
  .pipe(
    switchMap(async (challenge) => {
      // Trigger the browser's WebAuthn credential creation ceremony
      const credential = await navigator.credentials.create({
        publicKey: {
          ...challenge.authn_params_public_key,
          challenge: base64urlToBuffer(challenge.authn_params_public_key.challenge),
          user: {
            ...challenge.authn_params_public_key.user,
            id: base64urlToBuffer(challenge.authn_params_public_key.user.id),
          },
        },
      });

      return this.auth.myAccount.enrollmentVerify({
        type: 'passkey',
        location: challenge.location,
        auth_session: challenge.auth_session,
        authn_response: serializeCredential(credential), // serialize to PasskeyCredentialResponse
      });
    })
  )
  .subscribe({ next: (method) => console.log('Passkey enrolled:', method) });

Note

base64urlToBuffer and serializeCredential are platform-specific helpers you provide. The SDK does not handle the WebAuthn browser API directly — it handles the Auth0 challenge and token exchange on both sides.

Enroll TOTP

import { switchMap } from 'rxjs';

this.auth.myAccount
  .enrollmentChallenge({ type: 'totp' })
  .pipe(
    switchMap(async (challenge) => {
      // challenge.barcode_uri   — render as a QR code for the user to scan
      // challenge.manual_input_code — fallback code for manual entry
      showQrCode(challenge.barcode_uri);

      const otpCode = await promptUserForOtp(); // your UI to collect the OTP

      return this.auth.myAccount.enrollmentVerify({
        type: 'totp',
        location: challenge.location,
        auth_session: challenge.auth_session,
        otp_code: otpCode,
      });
    })
  )
  .subscribe({ next: (method) => console.log('TOTP enrolled:', method) });

Enroll Phone

import { switchMap } from 'rxjs';

this.auth.myAccount
  .enrollmentChallenge({
    type: 'phone',
    phone_number: '+15551234567',
    preferred_authentication_method: 'sms', // or 'voice'
  })
  .pipe(
    switchMap(async (challenge) => {
      const otpCode = await promptUserForOtp();

      return this.auth.myAccount.enrollmentVerify({
        type: 'phone',
        location: challenge.location,
        auth_session: challenge.auth_session,
        otp_code: otpCode,
      });
    })
  )
  .subscribe({ next: (method) => console.log('Phone enrolled:', method) });

Enroll Email

import { switchMap } from 'rxjs';

this.auth.myAccount
  .enrollmentChallenge({ type: 'email', email: 'user@example.com' })
  .pipe(
    switchMap(async (challenge) => {
      const otpCode = await promptUserForOtp();

      return this.auth.myAccount.enrollmentVerify({
        type: 'email',
        location: challenge.location,
        auth_session: challenge.auth_session,
        otp_code: otpCode,
      });
    })
  )
  .subscribe({ next: (method) => console.log('Email enrolled:', method) });

Enroll Push Notification

import { switchMap } from 'rxjs';

this.auth.myAccount
  .enrollmentChallenge({ type: 'push-notification' })
  .pipe(
    switchMap((challenge) => {
      // challenge.barcode_uri — render as a QR code for the user to scan with Auth0 Guardian
      showQrCode(challenge.barcode_uri);

      // No OTP needed — user approves the enrollment on their device
      return this.auth.myAccount.enrollmentVerify({
        type: 'push-notification',
        location: challenge.location,
        auth_session: challenge.auth_session,
      });
    })
  )
  .subscribe({ next: (method) => console.log('Push enrolled:', method) });

Enroll Recovery Code

import { switchMap } from 'rxjs';

this.auth.myAccount
  .enrollmentChallenge({ type: 'recovery-code' })
  .pipe(
    switchMap((challenge) => {
      // challenge.recovery_code — display this to the user to save securely
      showRecoveryCode(challenge.recovery_code);

      return this.auth.myAccount.enrollmentVerify({
        type: 'recovery-code',
        location: challenge.location,
        auth_session: challenge.auth_session,
      });
    })
  )
  .subscribe({ next: (method) => console.log('Recovery code enrolled:', method) });

Enroll Password

import { switchMap } from 'rxjs';

this.auth.myAccount
  .enrollmentChallenge({ type: 'password' })
  .pipe(
    switchMap(async (challenge) => {
      const newPassword = await promptUserForPassword();

      return this.auth.myAccount.enrollmentVerify({
        type: 'password',
        location: challenge.location,
        auth_session: challenge.auth_session,
        new_password: newPassword,
      });
    })
  )
  .subscribe({ next: (method) => console.log('Password enrolled:', method) });

MyAccount Error Handling

All MyAccount API errors are thrown as MyAccountApiError with RFC 7807 fields: status, title, detail, and an optional validation_errors array.

import { MyAccountApiError } from '@auth0/auth0-angular';
import { catchError, EMPTY } from 'rxjs';

this.auth.myAccount
  .enrollmentChallenge({ type: 'totp' })
  .pipe(
    catchError((err) => {
      if (err instanceof MyAccountApiError) {
        console.error(err.status, err.title, err.detail);

        if (err.validation_errors) {
          err.validation_errors.forEach((e) =>
            console.error(`${e.field}: ${e.detail}`)
          );
        }
      }
      return EMPTY;
    })
  )
  .subscribe();

// Insufficient scope (403) is the most common error — ensure your token has the
// required scope for the operation:
//   read:me:authentication_methods    — getAuthenticationMethods, getAuthenticationMethod
//   create:me:authentication_methods  — enrollmentChallenge, enrollmentVerify
//   update:me:authentication_methods  — updateAuthenticationMethod
//   delete:me:authentication_methods  — deleteAuthenticationMethod
//   read:me:factors                   — getFactors

Enterprise Connect

Enterprise Connect lets a B2B SaaS layer enterprise SSO (SAML, OIDC federation) on top of its own auth server without replacing it. Auth0 acts as a relay: it authenticates the enterprise user against their IdP and returns an enriched ID token, which the SDK caches like any other login.

Important

Enterprise Connect is an Early Access feature. The tenant setup (entitlements, connection type, and the claims a token carries) depends on your Auth0 configuration and may change. Confirm the tenant-side requirements with your Auth0 contact. The SDK surface described here is stable.

How the flow works

  1. The user enters their email. Your app calls isFederatedDomain with the email domain to run WebFinger discovery.
  2. If the domain is managed by Auth0 for enterprise SSO, call loginWithRedirect with the email as login_hint so Auth0 can resolve the connection and organization. If it is not managed, fall back to your own login.
  3. The user authenticates at their identity provider and is redirected back to your callback.
  4. Your app handles the redirect exactly as in a normal login. The ID token is verified and cached; read the claims from idTokenClaims$ / user$.

Important

isFederatedDomain is a routing hint, not a security control. It returns false on any failure (a 429, a network error, or a genuinely unmanaged domain all look the same), so a discovery failure routes the user to your fallback login rather than granting access. It never, on its own, signs anyone in: the callback must still complete, and you must still validate the resulting claims (see Validate the organization).

Configure the SDK

AuthModule.forRoot({
  domain: 'YOUR_AUTH0_DOMAIN',
  clientId: 'YOUR_AUTH0_CLIENT_ID',
  enterpriseConnect: true, // lets the SDK warn at init if the config contradicts EC's constraints
  authorizationParams: {
    redirect_uri: window.location.origin,
    scope: 'openid profile email', // no offline_access -- EC issues no refresh token
    // Do not set organization -- HRD resolves it from login_hint
  },
}),

Set enterpriseConnect: true to enable Enterprise Connect mode. The SDK then warns you at startup if your config contradicts EC's constraints.

Important

Enterprise Connect issues no refresh token, so the access token expires (24 hours by default) with no way to renew it silently. Treat EC as identity only: read the claims from the ID token (idTokenClaims$ / user$) and mint your own application session or API tokens from them. Do not forward the Auth0 access token to your own APIs for long-lived authorization, and check exp / expires_at if you cache it.

Log in

isFederatedDomain is a standalone function re-exported from @auth0/auth0-angular (it is not a method on AuthService). Pass your Auth0 domain and the email domain. If the domain is managed, start the redirect with the email as login_hint:

import { Component } from '@angular/core';
import { AuthService, isFederatedDomain } from '@auth0/auth0-angular';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
})
export class LoginComponent {
  constructor(public auth: AuthService) {}

  async login(email: string): Promise<void> {
    const emailDomain = email.split('@')[1];

    // 1. Discover whether the domain is managed for enterprise SSO.
    const federated = await isFederatedDomain('YOUR_AUTH0_DOMAIN', emailDomain);

    if (!federated) {
      // Domain is not managed by Auth0; fall back to your own login.
      this.showPasswordForm(email);
      return;
    }

    // 2. Redirect to Auth0 with the email as login_hint. Home Realm Discovery
    //    resolves the connection and organization from the domain -- do not
    //    pass organization yourself, or you break multi-customer setups.
    this.auth
      .loginWithRedirect({
        authorizationParams: { login_hint: email },
      })
      .subscribe();
  }

  // Your own login UI for domains that are not federated (e.g. show a
  // password field). Replace with your implementation.
  private showPasswordForm(email: string): void {}
}

isFederatedDomain accepts an optional third argument (IsFederatedDomainOptions) with customFetch and telemetry fields, mirroring auth0-spa-js.

Handle the callback

No changes to your existing callback handling. The SDK processes the redirect automatically; read the claims once authenticated:

import { Component } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';

@Component({
  selector: 'app-callback',
  template: '',
})
export class CallbackComponent {
  constructor(public auth: AuthService) {}

  // claims.org_id is the resolved organization.
  claims$ = this.auth.idTokenClaims$;
}

Validate the organization

Validating org_id is an application-level authorization decision, not something the SDK enforces. WebFinger discovery and login_hint only route the user to the right login; they don't prove the user belongs to one of your customers. If your app serves specific organizations, we recommend reading org_id from the ID token claims and checking it against your own list before treating the user as signed in for that customer.

import { filter, switchMap, take, throwError } from 'rxjs';

// `allowedOrgs` is a placeholder for illustration -- replace it with your
// own list of org_id values that this app is allowed to serve.
const allowedOrgs = ['org_123', 'org_456'];

this.auth.idTokenClaims$
  .pipe(
    // Ignore the null from an unauthenticated state; only validate a real claim set.
    filter((claims) => !!claims),
    take(1),
    switchMap((claims) => {
      if (!allowedOrgs.includes(claims.org_id)) {
        // The user authenticated via the enterprise IdP, so use a federated
        // logout here too, otherwise the IdP session survives the rejection.
        return this.auth
          .logout({
            logoutParams: { federated: true, returnTo: window.location.origin },
          })
          .pipe(switchMap(() => throwError(() => new Error('User does not belong to this organization'))));
      }
      return [claims];
    })
  )
  .subscribe();

This check runs in the browser, so a user can bypass it. Use it only to decide what the UI shows. Your backend must re-check org_id on every API request before trusting the token. Even if you serve a single organization today, keeping the check stops other tenants' users from getting in the day you add a second customer.

Log out

EC logout must use federated: true to terminate the enterprise IdP session (SAML SLO). Without it the IdP session stays alive and the next login silently reuses the previous user:

this.auth
  .logout({
    logoutParams: {
      federated: true,
      returnTo: window.location.origin,
    },
  })
  .subscribe();

Ensure the returnTo URL is listed in your application's Allowed Logout URLs in the Auth0 Dashboard, otherwise Auth0 rejects the post-logout redirect.