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.
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.
Really good article. I’m trying to learn authentication with cookies in .Net and this is quite useful but I’m a bit confused with the GetUser method.
You do this var userClaims = User.Claims.Select(x => new UserClaim(x.Type, x.Value)).ToList();
I’m confused with the User.Claims syntax. You say you’re getting the users’s claims. How does that work? Does this mean that when using claims on an application you can attach the “.Claims” keyword to any model you want or there are some rules to follow?.
No. If you inspect the code, you will find the User property. On this property, you can call the Claims property. Internal authenticating mechanisms will pars all the auth data into the User property, and again, if you debug the code, you can find all the info you can extract from it.
Ooooh I see, my bad. User is a ClaimsPrincipal object, I didn’t know that. I guess that when you do User.Claims.Select it’s going to take the claims you created in the SignInAsync method when the user logged in. Thanks for your response.
By the way, Doesn’t the method SignInAsync already includes a cookie in the response with the claims?. Do you really need the method GetUser to obtain the claims?.
This is for us to extract them and work with them. Maybe you need to check one of the claims and do something else based on the value of that claim. For that, you need to extract it using the User.Claims.
How can we implement refresh logic here?
Thank you for this. It is hard to find tutorials for custom cookie authentication on SPA frameworks.
Could y’all do the same tutorial with Blazor WASM instead of Angular?
Hello. We will see about the Blazor part, but to be honest, once you know the principles from one article, it isn’t hard to implement them into another framework. The API part is the main goal here, and it is already explained in this article. Blazor is just another framework on the client side so you can use this knowledge for sure. I did the same with the refresh token article that we had with Angular to create a Blazor article.
Thanks for your tutorial. However I get an issue, once sign in succeed, then call authorized endpoint API, in this second call I get 401. To my knowledge it’s caused by CORS mechanism. My URL API is https://localhost:7062, and client app URL is https://localhost:4200.
I have set the CORS, but after login it doesn’t set cookie in my client application yet. Can you advice me the solution. Thanks
I don’t believe it is a CORS issue. If it was CORS, you wouldn’t be able to send the sign-in request at all. It should’ve been rejected due to different URIs. Also, CORS will not return 401. 401 is Unauthorized, which means that your API returns that error when you try to access the protected action. Why is that? Well, I really don’t know, maybe something is wrong with the cookie or the auth configuration… or anything else. This is something I can’t know.