This post is a continuation of a series of posts that follow my initial looking into using IdentityServer4 in ASP.NET Core with an API and an Angular front end. The following are the related posts.
Identity Server: Introduction
Identity Server: Sample Exploration and Initial Project Setup
Identity Server: Interactive Login using MVC
Identity Server: From Implicit to Hybrid Flow
Identity Server: Using ASP.NET Core Identity
Identity Server: Using Entity Framework Core for Configuration Data
Identity Server: Usage from Angular (this post)
This post is finally going to add login from Angular in the Client Application. It has been a long time coming and will be a starting point, based on a few examples I found which I will list at the end. The starting point of the code can be found here.
API Application
In order for the Client Application to be able to call the API Application, there are some changes needed to allow cross-origin resource sharing. For more details check out this post only the basics will be covered here. First, add the following NuGet package.
- Microsoft.AspNetCore.Cors
Next, in the ConfigureServices function of the Startup class add AddCors before AddMvc. The following is the full function. This allows calls to the API Application from the Client Application which is running on localhost on port 5002.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("default", policy =>
{
policy.WithOrigins("http://localhost:5002")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddMvc();
}
Then, in the Configure function add app.UseCors(“default”); to make sure the default policy defined above is enforced. The following is the full function.
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseCors("default");
app
.UseIdentityServerAuthentication(
new IdentityServerAuthenticationOptions
{
Authority = "http://localhost:5000",
RequireHttpsMetadata = false,
ApiName = "apiApp"
});
app.UseMvc();
}
Identity Application
The Identity Application doesn’t have a lot of changes, but some of the configuration between it and the Client Application is what took me the bulk of time getting the code for this post setup and going.
If you are keeping up with the series then last you will know last week all the configuration data was moved to a database using Entity Framework Core. This is a bit of a problem now that I need to add a new client and the configuration data doesn’t any associated UI. To work around this I just added the new client to the Config class in the GetClients function and then deleted the existing database and let Entity Framework recreate it based on the new seed data. Not optimal, but I didn’t want to complicate things by adding a UI for the client setup. The following is the new client.
new Client
{
ClientId = "ng",
ClientName = "Angular Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RequireConsent = true,
RedirectUris = { "http://localhost:5002/callback" },
PostLogoutRedirectUris = { "http://localhost:5002/home" },
AllowedCorsOrigins = { "http://localhost:5002" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"apiApp"
},
}
There are a few things in this configuration that took me some time to get right. First of all best, I have been able to tell with this style of application implicit flow is the way to go which is handled by using a GrantTypes.Implicit for the AllowedGrantTypes.
The next issues I ran was a cross-origin resource sharing issue. Thankfully IdentityServer makes it easy to specify what origins should be allowed using the AllowedCorsOrigins property. In this example, we want to allow requests from the URL of our Client Application which is http://localhost:5002.
The last issue I had on was with the URIs I had set. The configuration in IdentityServer needs to exactly match the setup in the Client Application or you will have issues. I also had trouble trying to use the raw base address (http://localhost:5002) as the PostLogoutRedirectUris so look out for that as well.
Client Application
In the client application open the package.json file and add the following the dependencies section.
"oidc-client": "1.3.0",
"babel-polyfill": "6.23.0"
I also updated the typescript version to 2.3.4. Be cautious when changing the version of typescript as there is an issue with Angular and typescript 2.4.x at the moment.
At this point in the process, I had to find some resources on how to continue. The following are the ones I leaned on most.
ANGULAR OPENID CONNECT IMPLICIT FLOW WITH IDENTITYSERVER4
ASP.NET Core & Angular2 + OpenID Connect using Visual Studio Code
Repo for the previous link
Repo for with example Angular OidcClient
Getting this part of the application working involved a lot of changes and instead of going in depth on everything I am going to recommend just copying in the following files for the finished example code and dig more into them after you get an example working. Here is the list of files.
- ClientApp/ClientApp/app/components/callback/callback.component.ts
- ClientApp/ClientApp/app/components/services/ – whole directory
- ClientApp/ClientApp/boot-server.ts – related to a typescript error only if needed
With the above files in place, we will now focus on using the functionality they provide to log in and protect routes. To begin  app.module.client.ts, app.module.server.ts and app.module.shared.ts all need the next set of changes. I haven’t tried it yet, but I bet this change could just be made in the shared file and used in the other two. Add the following imports.
import { AuthService } from './components/services/auth.service';
import { GlobalEventsManager } from './components/services/global.events.manager';
import { AuthGuardService } from './components/services/auth-guard.service';
Next, add the same three items to the array of providers (or add one if it doesn’t exist). The following is an example from the shared file.
@NgModule({
bootstrap: sharedConfig.bootstrap,
declarations: sharedConfig.declarations,
imports: [
ServerModule,
...sharedConfig.imports
],
providers: [
AuthService, AuthGuardService, GlobalEventsManager
]
})
Finally, in the shared file change any routes that you would like to require the user to be logged in to be like the following which utilizes the canActivate of the route.
{ path: 'fetch-data',
component: FetchDataComponent,
canActivate:[AuthGuardService] }
In the navmenu.component.html which is the UI for the navigation menu add the following two options to the unordered list.
<li *ngIf="!_loggedIn" [routerLinkActive]="['link-active']">
<a (click)="login()" [routerLink]="['/login']">
<span class="glyphicon glyphicon-user"></span> Login
</a>
</li>
<li *ngIf="_loggedIn" [routerLinkActive]="['link-active']">
<a (click)="logout()" [routerLink]="['/logout']">
<span class='glyphicon glyphicon-log-out'></span> Logout
</a>
</li>
The user will only ever see one of the above options based on being logged in or not which is what the *ngIf is doing.
The navigation view model (navmenu.component.ts) changed a bit more. The following is the complete file.
import { Component } from '@angular/core';
import { AuthService } from '../services/auth.service'
import { GlobalEventsManager } from '../services/global.events.manager'
@Component({
selector: 'nav-menu',
templateUrl: './navmenu.component.html',
styleUrls: ['./navmenu.component.css']
})
export class NavMenuComponent {
public _loggedIn: boolean = false;
constructor (
private _authService: AuthService,
private _globalEventsManager: GlobalEventsManager) {
_globalEventsManager.showNavBarEmitter.subscribe((mode)=>{
// mode will be null the first time it is created, so you need to igonore it when null
if (mode !== null) {
console.log("Global Event, sent: " + mode);
this._loggedIn = mode;
}
});
}
public login(){
this._authService.startSigninMainWindow();
}
public logout(){
this._authService.startSignoutMainWindow();
}
}
New imports were added for the AuthService and GlobalEventsManager which get injected into the constructor of the class. The class also contains a _loggedIn property to track if the user is logged in or not. Finally, functions were added for login and logout to go with the two new links shown in the navigation.
Wrapping up
With the above, the Client Application can now log a user in and out utilizing IdentityServer from Angular. There are a lot of details in the files we just copied, but with a working sample, it is much easier to examine/customize how the process is being handled. Check back next week to see how to call the API Application from the Angular part of the Client Application.
The completed code can be found here.