I'm using Microsoft's scaffolded example with Angular + ASP.NET Core Identity with IdentityServer4. It uses their own package Microsoft.AspNetCore.ApiAuthorization.IdentityServer.
dotnet new angular -o <output_directory_name> -au Individual
It redirects to MVC login/register pages by default but I want to use my angular login/register pages instead of the MVC views. Is that possible? My angular and ASP.NET Core projects are completely separate and I want to keep the API as just API and at the same time, I like what microsoft did in that scaffolded project. They basically implemented code with PKCE along with the angular OIDC client which takes a lot of time to implement yourself and their implementation looks amazing.
Here is the default login.component.ts from the scaffolded example:
import { Component, OnInit } from '@angular/core';
import { AuthorizeService, AuthenticationResultStatus } from '../authorize.service';
import { ActivatedRoute, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { LoginActions, QueryParameterNames, ApplicationPaths, ReturnUrlType } from '../api-authorization.constants';
// The main responsibility of this component is to handle the user's login process.
// This is the starting point for the login process. Any component that needs to authenticate
// a user can simply perform a redirect to this component with a returnUrl query parameter and
// let the component perform the login and return back to the return url.
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
public message = new BehaviorSubject<string>(null);
constructor(
private authorizeService: AuthorizeService,
private activatedRoute: ActivatedRoute,
private router: Router) { }
async ngOnInit() {
const action = this.activatedRoute.snapshot.url[1];
switch (action.path) {
case LoginActions.Login:
await this.login(this.getReturnUrl());
break;
case LoginActions.LoginCallback:
await this.processLoginCallback();
break;
case LoginActions.LoginFailed:
const message = this.activatedRoute.snapshot.queryParamMap.get(QueryParameterNames.Message);
this.message.next(message);
break;
case LoginActions.Profile:
this.redirectToProfile();
break;
case LoginActions.Register:
this.redirectToRegister();
break;
default:
throw new Error(`Invalid action '${action}'`);
}
}
private async login(returnUrl: string): Promise<void> {
const state: INavigationState = { returnUrl };
const result = await this.authorizeService.signIn(state);
this.message.next(undefined);
switch (result.status) {
case AuthenticationResultStatus.Redirect:
break;
case AuthenticationResultStatus.Success:
await this.navigateToReturnUrl(returnUrl);
break;
case AuthenticationResultStatus.Fail:
await this.router.navigate(ApplicationPaths.LoginFailedPathComponents, {
queryParams: { [QueryParameterNames.Message]: result.message }
});
break;
default:
throw new Error(`Invalid status result ${(result as any).status}.`);
}
}
private async processLoginCallback(): Promise<void> {
const url = window.location.href;
const result = await this.authorizeService.completeSignIn(url);
switch (result.status) {
case AuthenticationResultStatus.Redirect:
// There should not be any redirects as completeSignIn never redirects.
throw new Error('Should not redirect.');
case AuthenticationResultStatus.Success:
await this.navigateToReturnUrl(this.getReturnUrl(result.state));
break;
case AuthenticationResultStatus.Fail:
this.message.next(result.message);
break;
}
}
private redirectToRegister(): any {
this.redirectToApiAuthorizationPath(
`${ApplicationPaths.IdentityRegisterPath}?returnUrl=${encodeURI('/' + ApplicationPaths.Login)}`);
}
private redirectToProfile(): void {
this.redirectToApiAuthorizationPath(ApplicationPaths.IdentityManagePath);
}
private async navigateToReturnUrl(returnUrl: string) {
// It's important that we do a replace here so that we remove the callback uri with the
// fragment containing the tokens from the browser history.
await this.router.navigateByUrl(returnUrl, {
replaceUrl: true
});
}
private getReturnUrl(state?: INavigationState): string {
const fromQuery = (this.activatedRoute.snapshot.queryParams as INavigationState).returnUrl;
// If the url is comming from the query string, check that is either
// a relative url or an absolute url
if (fromQuery &&
!(fromQuery.startsWith(`${window.location.origin}/`) ||
/\/[^\/].*/.test(fromQuery))) {
// This is an extra check to prevent open redirects.
throw new Error('Invalid return url. The return url needs to have the same origin as the current page.');
}
return (state && state.returnUrl) ||
fromQuery ||
ApplicationPaths.DefaultLoginRedirectPath;
}
private redirectToApiAuthorizationPath(apiAuthorizationPath: string) {
// It's important that we do a replace here so that when the user hits the back arrow on the
// browser they get sent back to where it was on the app instead of to an endpoint on this
// component.
const redirectUrl = `${window.location.origin}${apiAuthorizationPath}`;
window.location.replace(redirectUrl);
}
}
interface INavigationState {
[ReturnUrlType]: string;
}