Skip to content

Instantly share code, notes, and snippets.

@JaimeStill
Last active December 12, 2021 10:42
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save JaimeStill/468fca767ffabb977346eff81e2ace0c to your computer and use it in GitHub Desktop.
Save JaimeStill/468fca767ffabb977346eff81e2ace0c to your computer and use it in GitHub Desktop.
Active Directory Authorization Workflow

Active Directory Authorization Workflow

Contents

Overview

In an effort to keep this document relevant to the core subject matter, I will not be going into any detail on Entity Framework, Web API, or Angular. They already have amazing documentation, and I highly encourage you to read up on anything you're not familiar with if this doesn't make sense to you.

ASP.NET Core Docs
Entity Framework Docs
Angular Docs
Angular Material Docs

The intent of this document is to build on the Active Directory Authentication setup provided previously. It will include the following features:

  • Setting up Entity Framework Code First with a custom User entity
  • Building an API around AdUser and User
  • Setting up an Authorization guard for API endpoints
  • Building an Angular service for interacting with the API
  • Setting up an Angular Authentication workflow
  • Building an Angular route guard

This will only feature a simple .IsAdmin boolean property on the User class, but you could extend it to work with a Permission class with a UserPermission join table for a more robust authorization scheme.

Throughout this document, when defining names or path segments that are up to the reader to define, they will be expressed inside of { }. This means to replace the value, to include the brackets, with whatever your value is. For instance, a path of ..\{Project}.Data could be ..\FullstackDemo.Data.

Setup

You'll need to have access to a SQL Server (a SQL Server Express instance is present if you have Visual Studio installed with the Data workload) and work from a machine that's joined to an Active Directory domain.

I have a dotnet new template that serves as good starting point for getting this running on GitHub. Just follow the instructions in the README to install the template.

To create a project, open a command prompt and create / navigate to a directory you want to build the project in, then run dotnet new fullstack. This will create the project with the name of the directory it's hosted in. For example, if the name of the directory is Project, then the directory structure will look as follows:

  • Project.Core
  • Project.Web
  • Project.sln

Alternatively, you can run the command as dotnet new fullstack -n {Project Name} -o {Output Directory}

From the root of the directory you just created, run the following commands:

// Create new projects
{Project}>dotnet new classlib -f netcoreapp2.2 -n {Project}.Data -o {Project}.Data
{Project}>dotnet sln add .\{Project}.Data
{Project}>dotnet add classlib -f netcoreapp2.2 -n {Project}.Identity -o {Project}.Identity
{Project}>dotnet sln add .\{Project}.Identity

// Add Data references
{Project}>cd {Project}.Data
{Project}.Data>dotnet add package Microsoft.EntityFrameworkCore.SqlServer
{Project}.Data>dotnet add package Microsoft.EntityFrameworkCore.Tools
{Project}.Data>dotnet add reference ..\{Project}.Core

// Add Identity references
{Project}.Data> cd ..\{Project}.Identity
{Project}.Identity>dotnet add package Microsoft.AspNetCore.Http
{Project}.Identity>dotnet add package Microsoft.Extensions.Configuration.Abstractions
{Project}.Identity>dotnet add package Microsoft.Extensions.Configuration.Binder
{Project}.Identity>dotnet add package System.DirectoryServices.AccountManagement

// Add Web references
{Project}.Data>cd ..\{Project}.Web
{Project}.Web>dotnet add reference ..\{Project}.Data
{Project}.Web>dotnet add reference ..\{Project}.Identity

Delete both of the Class1.cs files out of both the {Project}.Data and {Project}.Identity classes.

At this point, go ahead and build out all of the infrastructure for {Project}.Identity as defined in the Active Directory Authentication document. Note that launchSettings.json is located at {Project}.Web\Properties\launchSettings.json.

Entity Framework Setup

{Project}.Web\appsettings.Development.json already contains a connection string. The only modification you should need to make is to update the value of Server={instance} to match the instance of the SQL Server you'll be using. (localdb)\\ProjectsV13 is installed by default if you have SQL Server Express installed from Visual Studio.

Create the following directory structure in {Project}.Data:

  • Entities
  • Extensions
  • AppDbContext.cs

{Project}.Data\Entities\User.cs

The SocketName property is relevant for when using SignalR to send direct messages to users in a Hub.

namespace Project.Data.Entities
{
    public class User
    {
        public int Id { get; set; }
        public Guid Guid { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Username { get; set; }
        public string Email { get; set; }
        public string SocketName { get; set; }
        public string Theme { get; set; }
        public bool IsDeleted { get; set; }
        public bool IsAdmin { get; set; }
    }
}

{Project}.Data\AppDbContext.cs

namespace Project.Data
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
        
        public DbSet<User> Users { get; set; }
        
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            /*
            * This causes table names in SQL Server to be their singular class name
            * as opposed to the plural DbSet<T> name.
            * 
            * In this case, the table name will be User instead of Users.
            */
            modelBuilder
                .Model
                .GetEntityTypes()
                .ToList()
                .ForEach(x =>
                {
                    modelBuilder
                        .Entity(x.Name)
                        .ToTable(x.Name.Split('.').Last());
                });
        }
    }
}

{Project}.Data\Extensions\IdentityExtensions.cs

namespace Project.Data.Extensions.IdentityExtensions
{
    public static async Task<List<User>> GetUsers(this AppDbContext db, bool isDeleted = false)
    {
        var users = await db
            .Users
            .Where(x => x.IsDeleted == isDeleted)
            .OrderBy(x => x.LastName)
            .ToListAsync();
            
        return users;
    }
    
    public static async Task<List<User>> SearchUsers(this AppDbContext db, string search, bool isDeleted = false)
    {
        search = search.ToLower();
        
        var users = await db
            .Users
            .Where(x => x.IsDeleted == isDeleted)
            .Where(x =>
                x.Email.ToLower().Contains(search) ||
                x.FirstName.ToLower().Contains(search) ||
                x.LastName.ToLower().Contains(search) ||
                x.Username.ToLower().Contains(search)
            )
            .OrderBy(x => x.LastName)
            .ToListAsync();
            
        return users;
    }
    
    public static async Task<User> GetUser(this AppDbContext db, int id)
    {
        var user = await db.Users.FindAsync(id);
        return user;
    }
    
    public static async Task<bool> CheckIsAdmin(this AppDbContext db, Guid guid)
    {
        var user = await db.Users
            .FirstOrDefaultAsync(x => x.Guid == guid);
            
        return user == null ? false : user.IsAdmin;
    }
    
    public static async Task<User> SyncUser(this AdUser adUser, AppDbContext db)
    {
        var user = await db
            .Users
            .FirstOrDefaultAsync(x => x.Guid == adUser.Guid);
            
        user = user == null ?
            await db.AddUser(adUser) :
            await db.UpdateUser(adUser);
            
        return user;
    }
    
    public static async Task<User> AddUser(this AppDbContext db, AdUser adUser)
    {
        User user = null;
        
        if (await adUser.Validate(db))
        {
            user = new User
            {
                Email = adUser.UserPrincipalName,
                FirstName = adUser.GivenName,
                Guid = adUser.Guid.Value,
                IsDeleted = false,
                LastName = adUser.Surname,
                SocketName = $@"{adUser.GetDomainPrefix()}\{adUser.SamAccountName}",
                Theme = "dark-green",
                Username = adUser.SamAccountName
            }
            
            await db.Users.AddAsync(user);
            await db.SaveChangesAsync();
        }
        
        return user;
    }
    
    public static async Task UpdateUser(this AppDbContext db, User user)
    {
        db.Users.Update(user);
        await db.SaveChangesAsync();
    }
    
    private static async Task<User> UpdateUser(this AppDbContext db, AdUser adUser)
    {
        var user = await db.Users.FirstOrDefaultAsync(x => x.Guid == adUser.Guid);
        
        user.Email = adUser.UserPrincipalName;
        user.FirstName = adUser.GivenName;
        user.LastName = adUser.Surname;
        user.SocketName = $@"{adUser.GetDomainPrefix()}\{adUser.SamAccountName}";
        user.Username = adUser.SamAccountName;
        
        await db.SaveChangesAsync();
        
        return user;
    }
    
    public static async Task ToggleUserDeleted(this AppDbContext db, User user)
    {
        db.Users.Attach(user);
        user.IsDeleted = !user.IsDeleted;
        await db.SaveChangesAsync();
    }

    public static async Task ToggleAdminUser(this AppDbContext db, User user)
    {
        db.Users.Attach(user);
        user.IsAdmin = !user.IsAdmin;
        await db.SaveChangesAsync();
    }
    
    public static async Task RemoveUser(this AppDbContext db, User user)
    {
        db.Users.Remove(user);
        await db.SaveChangesAsync();
    }
    
    public static async Task<bool> Validate(this AdUser user, AppDbContext db)
    {
        var check = await db
            .Users
            .FirstOrDefaultAsync(x => x.Guid == user.Guid.Value);
            
        if (check != null)
        {
            throw new Exception("The provided user already has an account");
        }
        
        return true;
    }
}

AppDbContext needs to be configured and registered as a service in {Project}.Web\Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddMvc()
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
        .AddJsonOptions(options =>
        {
            options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        });

    services.AddDbContext<AppDbContext>(options =>
    {
        options.UseSqlServer(Configuration.GetConnectionString("Dev"));
        options.EnableSensitiveDataLogging();
    });

    // Additional Configuration
}

Now all that's left for Entity Framework is to add a migration and update the database. From a command prompt pointed at {Project}.Data, run the following commands:

{Project}.Data>dotnet ef migrations add initial -s ..\Project.Web
{Project}.Data>dotnet ef database update -s ..\Project.Web

API Setup

In {Project}.Web, create a folder named Controllers and add a file named IdentityController.cs.

{Project}.Web\Controllers\IdentityController.cs

namespace Project.Web.Controllers
{
    [Route("api/[controller]")]
    public class IdentityController : Controller
    {
        private IUserProvider provider;
        private AppDbContext db;
        
        public IdentityController(IUserProvider provider, AppDbContext db, IConfiguration config)
        {
            this.provider = provider;
            this.db = db;
        }
        
        [HttpGet("[action]")]
        public async Task<List<AdUser>> GetDomainUsers() => await provider.GetDomainUsers();
        
        [HttpGet("[action]/{search}")]
        public async Task<List<AdUser>> FindDomainUser([FromRoute]string search) => await provider.FindDomainUser(search);
        
        [HttpGet("[action]")]
        public async Task<List<User>> GetUsers() => await db.GetUsers();
        
        [HttpGet("[action]")]
        public async Task<List<User>> GetDeletedUsers() => await db.GetUsers(true);
        
        [HttpGet("[action]/{search}")]
        public async Task<List<User>> SearchUsers([FromRoute]string search) => await db.SearchUsers(search);
        
        [HttpGet("[action]/{search}")]
        public async Task<List<User>> SearchDeletedUsers([FromRoute]string search) => await db.SearchUsers(search, true);
        
        [HttpGet("[action]/{id}")]
        public async Task<User> GetUser([FromRoute]int id) => await db.GetUser(id);
        
        [HttpGet("[action]")]
        public async Task<User> SyncUser() => await provider.CurrentUser.SyncUser(db);
        
        [HttpPost("[action]")]
        public async Task AddUser([FromBody]AdUser adUser) => await db.AddUser(adUser);
        
        [HttpPost("[action]")]
        public async Task UpdateUser([FromBody]User user) => await db.UpdateUser(user);
        
        [HttpPost("[action]")]
        public async Task ToggleUserDeleted([FromBody]User user) => await db.ToggleUserDeleted(user);

        [HttpPost("[action]")]
        public async Task ToggleAdminUser([FromBody]User user) => await db.ToggleAdminUser(user);
        
        [HttpPost("[action]")]
        public async Task RemoveUser([FromBody]User user) => await db.RemoveUser(user);
    }
}

This controller allows you to call the above messages and return data as JSON (where relevant) over HTTP. For instance, http://{localhost:5000}/api/identity/findDomainUser/jaime will return all users in the domain who have jaime as part of their UserPrincipal.SamAccountName. You can also POST JSON data to any of the POST methods, and it will be serialized to the appropriate C# object type (so long as the JSON signature matches the signature of the C# object).

API Route Guards

Create a folder in {Project}.Web named Authorization and add the following files:

  • AdminRequirement.cs
  • AdminAuthorizationHandler.cs

{Project}.Web\Authorization\AdminRequirement.cs

using AccountManager.Data;
using Microsoft.AspNetCore.Authorization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Project.Web.Authorization
{
    public class AdminRequirement : IAuthorizationRequirement
    {
    }
}

{Project}.Web\Authorization\AdminAuthorizationHandler.cs

using Microsoft.AspNetCore.Authorization;
using Project.Data.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Project.Web.Authorization
{
    public class AdminAuthorizationHandler : AuthorizationHandler<AdminRequirement>
    {
        AdUser user;
        AppDbContext db;

        public AdminAuthorizationHandler(AdUserProvider provider, AppDbContext db)
        {
            user = provider.CurrentUser;
            this.db = db;
        }

        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement)
        {
            if (await db.CheckIsAdmin(user.Guid))
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }
        }
    }
}

Now, Authorization needs to be configured in {Project}.Web\Startup.cs:

{Project}.Web\Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // Additional Configuration

    services.AddAuthorization(options =>
    {
        options.AddPolicy("IsAdmin", policy => policy.Requirements.Add(new AdminRequirement());
    });

    services.AddScoped<IAuthorizationHandler, AdminAuthorizationHandler>();

    // Additional Configuration
}

With this infrastructure in place, you can now use the [Authorize] attribute in conjunction with this new AdminAuthorizationHandler.

[Authorize(Policy = "IsAdmin")]
[HttpPost("[action]")]
public async Task ToggleAdminUser([FromBody]User user) => await db.ToggleAdminUser(user);

If the currently logged in user is not an admin, they will not be able to execute the ToggleAdminUser API route.

Angular Service

There's a folder convention that I use in Angular that makes managing / referencing each facet of the app much easier. Inside of the {Project}.Web\ClientApp\src\app folder, there is a folder for each Angular structure that contains an index.ts file. Any file contained within a folder must reference another file contained in the folder directly, or it will create a circular dependency loop. However, any file contained outside of this folder can reference files using the folder index.

For instance, inside of a service, you can reference multiple models in one import statement:

import { AdUser, User } from '../models';

However, a model must reference another model directly: import { AdUser} from './ad-user';

There are more benefits to this setup, especially concerning module configuration. If interested, take a look at the index.ts file in each folder, then take a look at both app.module.ts and services.module.ts to see how this setup is used.

In order to create the service, we need to have TypeScript types that will map to the C# types it will interact with.

{Project}.Web/ClientApp/src/app/models/ad-user.ts

export class AdUser {
  accountExpirationDate: Date;
  accountLockoutTime: Date;
  badLogonCount: number;
  description: string;
  displayName: string;
  distinguisedName: string;
  emailAddress: string;
  employeeId: string;
  enabled: boolean;
  givenName: string;
  guid: string;
  homeDirectory: string;
  homeDrive: string;
  lastBadPasswordAttempt: Date;
  lastLogon: Date;
  lastPasswordSet: Date;
  middleName: string;
  name: string;
  passwordNeverExpires: boolean;
  passwordNotRequired: boolean;
  samAccountName: string;
  scriptPath: string;
  sid: string;
  surname: string;
  userCannotChangePassword: boolean;
  userPrincipalName: string;
  voiceTelephoneNumber: string;
}

{Project}.Web\ClientApp\src\app\models\user.ts

export class User {
  id: number;
  guid: string;
  firstName: string;
  lastName: string;
  username: string;
  email: string;
  socketName: string;
  theme: string;
  isDeleted: boolean;
  isAdmin: boolean;
}

{Project}.Web\ClientApp\src\app\models\index.ts

export * from './ad-user';
export * from './user';

With the models defined, we can now build out the Angular service:

{Project}.Web\ClientApp\src\app\services\identity.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject } from 'rxjs';
import { SnackerService } from './snacker.service';

import {
  AdUser,
  User
} from '../models';

@Injectable()
export class IdentityService {
  private domainUsers = new BehaviorSubject<AdUser[]>(null);
  private userGroups = new BehaviorSubject<string[]>(null);
  private users = new BehaviorSubject<User[]>(null);
  private user = new BehaviorSubject<User>(null);
  private currentUser = new BehaviorSubject<User>(null);
  
    
  domainUsers$ = this.domainUsers.asObservable();
  userGroups$ = this.userGroups.asObservable();
  users$ = this.users.asObservable();
  user$ = this.user.asObservable();
  currentUser$ = this.currentUser.asObservable();
    
  constructor(
    private http: HttpClient,
    private snacker: SnackerService
  ) { }
    
  getDomainUsers = () =>
    this.http.get<AdUser[]>('/api/identity/getDomainUsers')
      .subscribe(
        data => this.domainUsers.next(data),
        err => this.snacker.sendErrorMessage(err.error)
      );
  
  findDomainUser = (search: string) =>
    this.http.get<AdUser[]>(`/api/identity/findDomainUser/${search}`)
      .subscribe(
        data => this.domainUsers.next(data),
        err => this.snacker.sendErrorMessage(err.error)
      );
  
  getUsers = () =>
    this.http.get<User[]>('/api/identity/getUsers')
      .subscribe(
        data => this.users.next(data),
        err => this.snacker.sendErrorMessage(err.error)
      );
  
  getDeletedUsers = () =>
    this.http.get<User[]>('/api/identity/getDeletedUsers')
      .subscribe(
        data => this.users.next(data),
        err => this.snacker.sendErrorMessage(err.error)
      );
  
  searchUsers = (search: string) =>
    this.http.get<User[]>(`/api/identity/searchUsers/${search}`)
      .subscribe(
        data => this.users.next(data),
        err => this.snacker.sendErrorMessage(err.error)
      );
  
  searchDeletedUsers = (search: string) =>
    this.http.get<User[]>(`/api/identity/searchDeletedUsers/${search}`)
      .subscribe(
        data => this.users.next(data),
        err => this.snacker.sendErrorMessage(err.error)
      );
  
  getUser = (id: number) =>
    this.http.get<User>(`/api/identity/getUser/${id}`)
      .subscribe(
        data => this.user.next(data),
        err => this.snacker.sendErrorMessage(err.error)
      );
  
  syncUser = () =>
    this.http.get<User>('/api/identity/syncUser')
      .subscribe(
        data => this.currentUser.next(data),
        err => this.snacker.sendErrorMessage(err.error)
      );
  
  addUser = (user: AdUser): Promise<boolean> =>
    new Promise<boolean>((resolve) => {
      this.http.post('/api/identity/addUser', user)
        .subscribe(
          () => {
            this.snacker.sendSuccessMessage(`Account created for ${user.samAccountName}`);
            resolve(true);
          },
          err => {
            this.snacker.sendErrorMessage(err.error);
            resolve(false);
          }
        )
    });
  
  updateUser = (user: User): Promise<boolean> =>
    new Promise<boolean>((resolve) => {
      this.http.post('/api/identity/updateUser', user)
        .subscribe(
          () => {
            this.snacker.sendSuccessMessage(`${user.username} successfully updated`);
            resolve(true);
          },
          err => {
            this.snacker.sendErrorMessage(err.error);
            resolve(false);
          }
        )
    });
  
  toggleUserDeleted = (user: User): Promise<boolean> =>
    new Promise<boolean>((resolve) => {
      this.http.post('/api/identity/toggleUserDeleted', user)
        .subscribe(
          () => {
            const message = user.isDeleted ?
              `${user.username} successfully restored` :
              `${user.username} successfully deleted`;
            
            this.snacker.sendSuccessMessage(message);
            resolve(true);
          },
          err => {
            this.snacker.sendErrorMessage(err.error);
            resolve(false);
          }
        )
    });

  toggleAdminUser = (user: User): Promise<boolean> =>
    new Promise<boolean>((resolve) => {
      this.http.post('/api/identity/toggleAdminUser', user)
        .subscribe(
          () => {
            const message = user.isAdmin ?
              `Permissions removed from ${user.username}` :
              `Permissions granted to ${user.username}`;
            
            this.snacker.sendSuccessMessage(message);
            resolve(true);
          },
          err => {
            this.snacker.sendErrorMessage(err.error);
            resolve(false);
          }
        )
    });
  
  removeUser = (user: User): Promise<boolean> =>
    new Promise<boolean>((resolve) => {
      this.http.post('/api/identity/removeUser', user)
        .subscribe(
          () => {
            this.snacker.sendSuccessMessage(`${user.username} permanently deleted`);
            resolve(true);
          },
          err => {
            this.snacker.sendErrorMessage(err.error);
            resolve(false);
          }
        )
    });
}

In this service, each API endpoint is defined as a function. For GET requests, the results are pushed into an Observable stream via a private BehaviorSubject<T>, which is exposed to the app as a read-only Observable. For POST requests, the HTTP call is wrapped in a Promise to enable async / await to be used by the caller and for any subsequent actions to be performed once the transaction is complete.

Angular Authentication Workflow

With all of this complete, we can now sync the current Active Directory user to our User database table and keep track of them in the Angular app.

All we need to do is the following in the AppComponent:

{Project}.Web\ClientApp\src\app\app.component.ts

import { Theme } from './models';
import { IdentityService } from './services';

import {
  Component,
  OnInit
} from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  providers: [ IdentityService ]
})
export class AppComponent implements OnInit {
  constructor(
    public identity: IdentityService
  ) { }

  ngOnInit() {
    this.identity.syncUser();
  }
}

When the component initializes, the identity service will call the syncUser() function. This will check to see if the current IIdentity on the HttpContext has an account in the database. If not, it will create a new account based on the UserPrincipal that is retrieved from the IIdentity instance. If the user does have an account, it will update their properties in the event that anything changed from the last time they logged in (for instance, someone was married and their name changed).

We can then subscribe to the currentUser$ Observable in the component template. We will only render the app if the current user is logged in via Active Directory.

<div class="mat-typography mat-app-background app-panel"
     fxLayout="column"
     *ngIf="identity.currentUser$ | async as user else loading">
  <mat-toolbar color="primary">
    <span fxFlex>Title</span>
    <span>Hello, {{user.username}}</span>
  </mat-toolbar>
  <section class="app-body">
    <router-outlet></router-outlet>
  </section>
</div>
<ng-template #loading>
  <mat-progress-bar mode="indeterminate"
                    color="secondary"></mat-progress-bar>
</ng-template>

Angular Route Guards

Now that we have access to the current user, we can build a route guard to prevent non-admins from accessing certain areas of the app.

In the {Project}.Web\ClientApp\src\app directory, create a guards folder with a file name auth-guard.ts:

{Project}.Web\ClientApp\src\app\guards\auth-guard.ts

import { Injectable } from '@angular/core';

import {
  CanActivate,
  Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot
} from '@angular/router';

import { IdentityService } from '../services';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor (
    private identity: IdentityService,
    private router: Router
  ) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): boolean {
    return this.checkLogin();
  }
  
  checkLogin = (): Promise<boolean> =>
    new Promise((resolve) => {
      this.identity.currentUser$.subscribe((user) => {
        if (user) {
          resolve(user.isAdmin);
          if (!user.isAdmin) this.router.navigate(['/home']);
        }
      });
    });
}

Create an index.ts file in the guards folder:

{Project}.Web\ClientApp\src\app\guards\index.ts

import { AuthGuard } from './auth-guard';

export const Guards = [
  AuthGuard
];

export * from './auth-guard';

Expand the Guards array into the providers array in services.module.ts:

{Project}.Web\ClientApp\src\app\services.module.ts

import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { Guards } from './guards';
import { Services } from './services';
import { Pipes } from './pipes';

@NgModule({
  providers: [
    [...Services],
    [...Guards]
  ],
  declarations: [
    [...Pipes]
  ],
  imports: [
    HttpClientModule
  ],
  exports: [
    [...Pipes],
    HttpClientModule
  ]
})
export class ServicesModule { }

Now, all you have to do in order to lock down a route in Angular is add the canActivate property to a route (as defined in {Project}.Web\ClientApp\src\app\routes\index.ts) as follows:

import { AuthGuard } from '../guards';

export const Routes: Route[] = [
  { path: 'admin', component: AdminComponent, canActive: [AuthGuard] },
  // additional routes...
]
@d4dpkrajput
Copy link

Is it complete code ? can i use it in my project ?

@brunoccsnasajon
Copy link

I'm lost. Where are the login routines? Shouldn't somewhere there be a Login route that accepts a user and password and validates it againt the Active Directory?

@JaimeStill
Copy link
Author

@brunoccsnasajon When Windows Authentication is enabled on IIS, the domain user logged into the machine is automatically provided to the app through IIS. Otherwise, if a user is not logged in (or pass-through authentication is not enabled), the browser will prompt for AD credentials. IIS is essentially a broker between the app and AD.

@marcofo23
Copy link

marcofo23 commented Jul 18, 2019

Hi @JaimeStill !
One thing is not clear to me, where do you define methods included in the class IdentityExtensions.cs?

Thanks for the code !!!

@marcofo23
Copy link

I solved thanks

@thopah
Copy link

thopah commented Aug 26, 2019

AdminComponent is not defined in this tutorial

@JaimeStill
Copy link
Author

AdminComponent is not defined in this tutorial

@thopah, AdminComponent is just a theoretical route component. It could be any component that you want to lock down with an AuthGuard.

@thopah
Copy link

thopah commented Aug 28, 2019

this is awesome. Thanks a lot. Could you also provide tutorials on using AD group to authorize user access. Thanks

@PrashantUbale2019
Copy link

PrashantUbale2019 commented Sep 18, 2019

Project with Dot Net Core in MSVS gives error for SyncUser:Invalid object name "User"
An unhandled exception occurred while processing the request.
SqlException: Invalid object name 'User'.
System.Data.SqlClient.SqlCommand+<>c.b__122_0(Task result)

WEB API in VS and angular in Code does not work for authentication gives: "Unauthorized" while two apps are differently running in different editors.
but "FindDomainUser" works for me.
I have mongodb document db for other operations and localdb in sqlexpress for user authentications as specified in article.

@JaimeStill
Copy link
Author

Project with Dot Net Core in MSVS gives error for SyncUser:Invalid object name "User"
An unhandled exception occurred while processing the request.
SqlException: Invalid object name 'User'.
System.Data.SqlClient.SqlCommand+<>c.b__122_0(Task result)

WEB API in VS and angular in Code does not work for authentication gives: "Unauthorized" while two apps are differently running in different editors.
but "FindDomainUser" works for me.
I have mongodb document db for other operations and localdb in sqlexpress for user authentications as specified in article.

@PrashantUbale2019, not knowing exactly how you're setup, it looks like the database isn't updated with your Entity Framework migrations. Make sure you create a migration and update the SQL database with your latest Entity Framework schema.

@PrashantUbale2019
Copy link

PrashantUbale2019 commented Sep 23, 2019

Yes, you are right, There was a error while running migration command, so I skipped it. Now it is success.
Still I have a question: Will it work If I deploy this app on server without AD tenant ID, and ClientID?
Does not it require client side authentication?
If user access the app from mobile / apart from AD / personal device then It has to login himself?
In this case how the app identify if we do not handle at client side authentication.
Please go through below link: https://www.npmjs.com/package/microsoft-adal-angular6

@thopah
Copy link

thopah commented Dec 10, 2019

Hi, I am getting the following error while trying to use the IsAdmin authorize police.

Unable to resolve service for type 'ActiveDirectoryAuthorizationWorkflow.Identity.AdUserProvider' while attempting to activate 'ActiveDirectoryAuthorizationWorkflow.Api.Authorization.AdminAuthorizationHandler'.

@BrendonOSullivan
Copy link

Can you upgrade FullStack template to .Net Core 3.1?

@JaimeStill
Copy link
Author

@BrendonOSullivan in the plans for after the holidays. Cheers

@JaimeStill
Copy link
Author

@BrendonOSullivan My app stack template is now being maintained here. The FullstackTemplate is still at .NET Core 2.2 to stay in sync with the FullstackOverview article. The linked template is at .NET Core 3.1 and the latest stable Angular release.

@BrendonOSullivan
Copy link

BrendonOSullivan commented Jan 5, 2020

@BrendonOSullivan My app stack template is now being maintained here.

Thanks @JaimeStill ! I downloaded the latest version and installed it as a project template. It works; I can create a new project, build and run it with no errors.

@JaimeStill
Copy link
Author

@BrendonOSullivan My app stack template is now being maintained here.

Thanks @JaimeStill ! I downloaded the latest version and installed it as a project template. It works; I can create a new project, build and run it with no errors.
@BrendonOSullivan Awesome! Enjoy using it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment