In this article, we’re going to learn about cookie authentication with ASP.NET Core and Angular.

We’ll create an ASP.NET Core Web API with sign-in, sign-out, and a protected user endpoint for demonstration. In Angular, we’ll access the protected user endpoint after successful authentication based on cookies.

To download the source code for this article, you can visit our GitHub repository.

Let’s begin.

What Are HTTP Cookies

A server transmits a small piece of data called an HTTP cookie (also known as a web cookie or browser cookie) to a user’s web browser. With subsequent requests, the browser may save the cookie and transmit it back to the same server.

Cookies help to identify if the request comes from the same browser. Hence, we use cookies for authentication purposes.

To learn more about cookies, please refer to this article.

How to Setup Cookie Authentication in ASP.NET Core

In one of our previous articles, we learned about using multiple authentication schemes in ASP.NET Core. In this article, we’ll focus mainly on cookie authentication.

First, let’s create a new project using ASP.NET Core with Angular project template in Visual Studio.

After that, we need to change the Program.cs to enable cookie authentication:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Events.OnRedirectToLogin = (context) =>
        {
            context.Response.StatusCode = 401;
            return Task.CompletedTask;
        };
    });

Here, the AddAuthentication method adds a default authentication scheme using an inbuilt CookieAuthenticationDefaults.AuthenticationScheme constant. After that, the AddCookie method adds the required services for cookie authentication.

By default, cookie authentication redirects the user to the login URL if authentication failed. Hence, we’re setting the delegate function options.Events.OnRedirectToLogin with a lambda expression. This expression returns an unauthorized HTTP status code 401.

After this is done, we need to enable the authentication in Program.cs:

app.UseAuthentication();
app.UseAuthorization();

app.MapDefaultControllerRoute();

Make sure to add the methods in the same order. The UseAuthentication and UseAuthorization methods add the required middleware to the pipeline to authenticate the user on each request. These middlewares will also set the User property in the controller.

Now we can start creating the endpoints. Let’s create a new controller named AuthController with ApiController and Route attributes:

[ApiController]
[Route("/api/auth")]
public class AuthController : Controller
{
}

Here, the ApiController attribute enables the API-specific behavior. With Route attribute, we specify the root API path /api/auth for all the action methods in this controller.

Let’s create the required models for our endpoints using a record type:

public record SignInRequest(string Email, string Password);
public record Response(bool IsSuccess, string Message);
public record UserClaim(string Type, string Value);
public record User(string Email, string Name, string Password);

Sign In Endpoint

For demonstration purposes, we’re creating a simple user store using a private field in AuthController. It will hold the hardcoded user information:

private List<User> users = new()
{
    new("[email protected]", "User 1", "user1"),
    new("[email protected]", "User 2", "user2"),
};

Now, let’s create the endpoint api/auth/signin as HTTP post method:

[HttpPost("signin")]
public async Task<IActionResult> SignInAsync([FromBody] SignInRequest signInRequest)
{
    var user = users.FirstOrDefault(x => x.Email == signInRequest.Email &&
                                        x.Password == signInRequest.Password);
    if (user is null)
    {
        return BadRequest(new Response(false, "Invalid credentials."));
    }

    var claims = new List<Claim>
    {
        new Claim(type: ClaimTypes.Email, value: signInRequest.Email),
        new Claim(type: ClaimTypes.Name,value: user.Name)
    };
    var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);

    await HttpContext.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,
        new ClaimsPrincipal(identity),
        new AuthenticationProperties
        {
            IsPersistent = true,
            AllowRefresh = true,
            ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
        });

    return Ok(new Response(true, "Signed in successfully"));
}

Here, with the signInRequest parameter, we receive the Email and Password values. Using these values, we’re checking our local user store users whether the given user exists or not.

If the user credentials are valid, we create claims and then the HTTP cookie using the HttpContext.SignInAsync method.

User Endpoint

Signed-in users can access the protected endpoints. Let’s create one protected GET endpoint /api/auth/user:

[Authorize]
[HttpGet("user")]
public IActionResult GetUser()
{
    var userClaims = User.Claims.Select(x => new UserClaim(x.Type, x.Value)).ToList();

    return Ok(userClaims);
}

In this endpoint, we’re returning the currently signed-in user’s claims. The Authorize attribute protects this endpoint. Since we’re using cookie authentication, it checks the cookie in the request’s header and populates the User object. If the cookie is invalid, it returns 401 HTTP status code automatically.

Sign Out Endpoint

Finally, let’s create a sign-out endpoint /api/auth/signout to clear the cookies:

[Authorize]
[HttpGet("signout")]
public async Task SignOutAsync()
{
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}

Here, the endpoint will clear the cookies from the user’s browser by issuing a new expired cookie without any claims data.

How to Authenticate Cookies in Angular

In this section, we will work with the Angular part of our application.

Auth Service

First, let’s create the necessary interfaces for the API responses:

export interface UserClaim {
    type: string;
    value: string;
}

export interface Response {
    isSuccess: boolean;
    message: string;
}

Now we can create the AuthService class to do all the necessary API calls:

@Injectable({
    providedIn: 'root',
})
export class AuthService {
    constructor(private http: HttpClient) {}

    public signIn(email: string, password: string) {
        return this.http.post<Response>('api/auth/signin', {
            email: email,
            password: password
        });
    }

    public signOut() {
        return this.http.get('api/auth/signout');
    }

    public user() {
        return this.http.get<UserClaim[]>('api/auth/user');
    }

    public isSignedIn(): Observable<boolean> {
        return this.user().pipe(
            map((userClaims) => {
                const hasClaims = userClaims.length > 0;
                return !hasClaims ? false : true;
            }),
            catchError((error) => {
                return of(false);
            }));
    }
}

In AuthService, we create signIn, user, and signOut functions. These functions call the respective API endpoints in the backend.

Apart from these functions, we create another isSignedIn function. This function calls the user endpoint and returns true or false based on its authentication status.

Sign In Component

Now, let’s create a sign-in component HTML to input user credentials:

<div class="d-flex flex-column align-items-center justify-content-center" *ngIf="!signedIn else signedInAlready">
    <div>Please Sign In to continue</div>
    <form class="form w-50" [formGroup]="loginForm" (submit)="signin($event)">
        <div class="row m-3">
            <label for="name">Email*</label>
            <input class="col form-field" name="email" formControlName="email" />
        </div>
        <div class="row m-3">
            <label for="password">Password*</label>
            <input class="col form-field" type="password" name="password" formControlName="password" autocomplete="on" />
        </div>
        <div class="row m-3" *ngIf="authFailed">
            <label style="color: red">Invalid credentials</label>
        </div>
        <div class="row m-3">
            <button class="col btn btn-primary" [disabled]="!loginForm.valid" type="submit">Sign In</button>
        </div>
    </form>
    <div class="alert-info p-2">
        Try [email protected] / user1
        or [email protected] / user2
    </div>
</div>

<ng-template #signedInAlready>
    <h4>Sign In</h4>
    <div>
        You are already signed in!
    </div>
</ng-template>

After that, let’s create the SignInComponent class:

@Component({
    selector: 'app-signin-component',
    templateUrl: './signin.component.html'
})
export class SignInComponent implements OnInit {
    loginForm!: FormGroup;
    authFailed: boolean = false;
    signedIn: boolean = false;

    constructor(private authService: AuthService,
        private formBuilder: FormBuilder,
        private router: Router) {
        this.authService.isSignedIn().subscribe(
            isSignedIn => {
                this.signedIn = isSignedIn;
            });
    }

    ngOnInit(): void {
        this.authFailed = false;
        this.loginForm = this.formBuilder.group(
            {
                email: ['', [Validators.required, Validators.email]],
                password: ['', [Validators.required]]
            });
    }

    public signIn(event: any) {
        if (!this.loginForm.valid) {
            return;
        }
        const userName = this.loginForm.get('email')?.value;
        const password = this.loginForm.get('password')?.value;
        this.authService.signIn(userName, password).subscribe(
            response => {
                if (response.isSuccess) {
                    this.router.navigateByUrl('user')
                }
            },
            error => {
                if (!error?.error?.isSuccess) {
                    this.authFailed = true;
                }
            });
    }
}

In this component, we are checking whether the user is signed in or not in the constructor. In ngOnInit function, we’re initializing the loginForm with two form fields email and password

Whenever the form is submitted, the signIn function gets called. In this function, we retrieve the form values and then pass them to the this.authService.signIn function. This function calls the /api/auth/signin endpoint and returns the result.

If the credentials are valid, the API sets the cookie in the response header with the key set-cookie:

set-cookie: .AspNetCore.Cookies=CfDJ8KZELxg4LUBBvlmGEWUFluV ... _w8capcAdZ3fvcL18VNo5fReX1juZAI; expires=Sun, 10 Jul 2022 06:34:18 GMT; path=/; secure; samesite=lax; httponly

If we look at the cookie, the user claims are encrypted. The browser stores it and pass this cookie in the subsequent request header with the key Cookie:

Cookie: .AspNetCore.Cookies=CfDJ8KZELxg4LUBBvlmGEWUFluVgrOiEyLK6GfUQIr8qlbJKQBcCANpte0iuC9uBZ-_zfJl-W...

User Component

Once the sign-in is successful, we redirect the user to another component called UserComponent:

@Component({
    selector: 'app-user',
    templateUrl: './user.component.html',
})
export class UserComponent {
    userClaims: UserClaim[] = [];
    constructor(private authService: AuthService) {
        this.getUser();
    }

    getUser() {
        this.authService.user().subscribe(
            result => {
                this.userClaims = result;
            });
    }
}

In this component, we call the /api/auth/user endpoint using the getUser() function.

If the cookie is valid, the protected user endpoint will return the 200 (Ok) response. Otherwise, the endpoint will return 401 (Unauthorized).

Sign Out Component

Now, let’s implement the SignOutComponent. It is as simple as calling the this.authService.signOut() function on click of a ‘Sign Out’ button:

@Component({
    selector: 'app-signout-component',
    templateUrl: './signout.component.html'
})
export class SignOutComponent {
    constructor(private authService: AuthService) {
        this.signout();
    }

    public signout() {
        this.authService.signOut().subscribe();
    }
}

As soon as the /api/auth/signout endpoint is called a cookie will be invalidated. The server does this by issuing expired cookies in the same set-cookie response header:

set-cookie: .AspNetCore.Cookies=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; secure; samesite=lax; httponly

So, calling any protected endpoint subsequently will return 401.

Auth Interceptor

So far, we didn’t handle the unauthorized response. Under any circumstance, if the user tries to access the protected endpoints, we should redirect them to the Sign In page. To achieve this, we can implement our custom HttpInterceptor.

Let’s create a AuthInterceptor class to intercept the request:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private router: Router) { }
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).pipe(tap(() => { },
            (err: any) => {
                if (err instanceof HttpErrorResponse) {
                    if (err.status !== 401) {
                        return;
                    }
                    this.router.navigate(['signin']);
                }
            }));
    }
}

Here, we’re intercepting the response of all the API calls using the pipe and tap RxJS operators. If any of the endpoints return 401, then we redirect the user to the sign-in page.

Auth Guard

The above solution works only if the page makes any request to the protected API endpoint.

To overcome this and to protect any angular route, we can implement a guard class:

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

    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
        return this.isSignedIn();
    }

    isSignedIn(): Observable<boolean> {
        return this.authService.isSignedIn().pipe(
            map((isSignedIn) => {
                if (!isSignedIn) {
                    this.router.navigate(['signin']);
                    return false;
                }
                return true;
            }));
    }
}

Here, the canActivate function checks for the sign-in status of the user. If signed in, we allow the user to access the page. Otherwise, redirect them to the sign-in page.

Routes can use our AuthGuard in its declaration using canActivate key:

RouterModule.forRoot([
    {
        path: 'secured',
        component: SecuredComponent,
        pathMatch: 'full',
        canActivate: [AuthGuard]
    }
])

Provider Registration

We need to register both AuthInterceptor and AuthGuard in the AppModule providers in order to take effect:

providers: [
    {
        provide: HTTP_INTERCEPTORS,
        useFactory: function (router: Router) {
            return new AuthInterceptor(router);
        },
        multi: true,
        deps: [Router]
    },
    AuthGuard
],

Testing the application

Finally, let’s run the application through Visual Studio or dotnet run command. In the browser, we see the ‘Sign In’ page to input the credentials.

When we try to use invalid credentials, we cannot sign in. For valid credentials, the server issues cookies to the browser and we redirect to the User page. On this page, we see the authenticated user’s claims:

[
  {
    "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
    "value": "[email protected]"
  },
  {
    "type": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
    "value": "User 1"
  }
]

Eventually, we can access the ‘Secured’ page as well. Once we click on the ‘Sign Out’ button, the server invalidates the cookie. Hence, we cannot access both the ‘User’ and ‘Secured’ pages after we sign out. At this point, the browser always redirects us to the ‘Sign In’ page to input valid credentials.

Conclusion

In this article, we’ve learned to use cookie authentication with ASP.NET Core and Angular in detail with a code sample. Initially, we looked at creating the endpoints. After that, we created the angular components, interceptors, and guards to properly authenticate the user.