MusicStore - Part4 - External Login
14 Mar 2019Let’s enable the external login, so that Microsoft and LinkedIn users don’t need to sign up in Music Store.
-
Microsoft
- Create an app in Microsoft via https://apps.dev.microsoft.com/
- Fill in Name
-
Generate New Password

- Add Platform

-
Update IdentityServer4 to authenticate Microsoft users
- Install NuGet Package - Microsoft.AspNetCore.Authentication.MicrosoftAccount
- Add Microsoft Authentication
// Startup.cs public void ConfigureServices(IServiceCollection services) { ...... services.AddAuthentication() .AddMicrosoftAccount(options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Microsoft:ApplicationId"]; options.ClientSecret = Configuration["Microsoft:Password"]; options.CallbackPath = new PathString("/microsoft"); options.SaveTokens = true; }); ...... } public void Configure(IApplicationBuilder app) { ...... app.UseAuthentication(); ...... }- Store Application Id and Password from #1.1 to appsettings.Development.json
{ ...... "Microsoft": { "ApplicationId": "0d1c72bb-ec52-4a0c-a582-4ad79f9a9c5a", "Password": "vnssBKZWR237*ujtZW44@*|" } ...... }
- Create an app in Microsoft via https://apps.dev.microsoft.com/
-
LinkedIn
-
Create an app in LinkedIn via https://www.linkedin.com/developers/apps
- Fill in App name, Company name, App description, Logo, and Business email

- Fill in App name, Company name, App description, Logo, and Business email
-
Update IdentityServer4 to authenticate LinkedIn users
- Install NuGet Package - AspNet.Security.OAuth.LinkedIn
-
Add LinkedIn Authentication
public void ConfigureServices(IServiceCollection services) { ...... services.AddAuthentication() .AddLinkedIn(options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["LinkedIn:ClientId"]; options.ClientSecret = Configuration["LinkedIn:ClientSecret"]; options.CallbackPath = new PathString("/linkedin"); options.SaveTokens = true; }); ...... } -
Store Client Id and Client Secret from #2.1 to appsettings.Development.json
{ ...... "LinkedIn": { "ClientId": "816crn5hi7uoxx", "ClientSecret": "lyV15uviOnli9tQr" } ...... }
-
-
Authenticate users from external OAuth 2.0 providers
-
Replace TestUserStore with AspNetIdentity UserManager and SignInManager
// ExternalController.cs private readonly UserManager<ApplicationUser> _userManager; private readonly SignInManager<ApplicationUser> _signInManager; public ExternalController( IIdentityServerInteractionService interaction, IClientStore clientStore, IEventService events, UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager ) { _interaction = interaction; _clientStore = clientStore; _events = events; _userManager = userManager; _signInManager = signInManager; } -
External login callback
// ExternalController.cs [HttpGet] public async Task<IActionResult> Callback() { // read external identity from the temporary cookie var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); if (result?.Succeeded != true) { throw new Exception("External authentication error"); } var extPrincipal = result.Principal; var expProperties = result.Properties; var claims = extPrincipal.Claims.ToList(); var userIdClaim = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject); if (userIdClaim == null) { userIdClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier); } if (userIdClaim == null) { throw new Exception("Unknown userid"); } claims.Remove(userIdClaim); var provider = expProperties.Items["scheme"]; var userId = userIdClaim.Value; var user = await _userManager.FindByLoginAsync(provider, userId); if (user == null) { // Register if user doesn't exist var candidateId = Guid.NewGuid(); user = new ApplicationUser(); var emailClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.Email); if (emailClaim != null) { user.UserName = emailClaim.Value; user.Email = emailClaim.Value; user.EmailConfirmed = true; } var firstNameClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.GivenName); if (firstNameClaim != null) { user.FirstName = firstNameClaim.Value; } var lastNameClaim = claims.FirstOrDefault(x => x.Type == ClaimTypes.Surname); if (lastNameClaim != null) { user.LastName = lastNameClaim.Value; } user.UserType = EnumUserType.Audience; var createResult = await _userManager.CreateAsync(user); if (createResult.Succeeded) { var loginResult = await _userManager.AddLoginAsync(user, new UserLoginInfo(provider, userId, provider)); if (loginResult.Succeeded) { // Update Tokens to AspNet Identity var externalLoginInfo = new ExternalLoginInfo(extPrincipal, provider, userId, provider) { AuthenticationTokens = expProperties.GetTokens() }; await _signInManager.UpdateExternalAuthenticationTokensAsync(externalLoginInfo); } } } ...... }
-
-
Display user information in WebUI
-
Update welcome message in nav-menu component
<pre class="wp-block-code"><code>// nav-menu.component.html <div class='main-nav'> <div class='navbar navbar-inverse'> <div class='navbar-header'> ...... <a class='navbar-brand' [routerLink]='["/"]'>Welcome </a> </div> ...... </div> </div>// nav-menu.component.ts import { Component } from '@angular/core'; import { UserProfileService } from '../userprofile/userprofile.service'; @Component({ selector: 'app-nav-menu', templateUrl: './nav-menu.component.html', styleUrls: ['./nav-menu.component.css'] }) export class NavMenuComponent { isExpanded = false; firstName: string = ''; lastName: string = ''; constructor( private _userProfileService: UserProfileService ) { var that = this; this._userProfileService.identityClaimsReady.subscribe(function (claims) { if (claims) { that.firstName = claims["FirstName"]; that.lastName = claims["LastName"]; } }); } ...... } -
Send claims to subscribers
// app.component.ts ...... import { UserProfileService } from './userprofile/userprofile.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { title = 'app'; constructor( @Inject(PLATFORM_ID) private platformId: Object, private _oauthService: OAuthService, private _userProfileService: UserProfileService ) { ...... } /** * On init */ ngOnInit(): void { if (isPlatformBrowser(this.platformId)) { this._oauthService.loadDiscoveryDocumentAndTryLogin().then(_ => { if (!this._oauthService.hasValidIdToken() || !this._oauthService.hasValidAccessToken()) { this._oauthService.initImplicitFlow(); } else { this._userProfileService.onIdentityClaimsReadyChanged(this._oauthService.getIdentityClaims()); } }); } } } -
Add userprofile.service.ts to observe claim changes
import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; @Injectable() export class UserProfileService { identityClaimsReady: Subject<any> = new Subject(); onIdentityClaimsReadyChanged(claims): void { this.identityClaimsReady.next(claims); } }
-
-
Include FirstName and LastName in Claims from IdentityServer4
// ProfileService.cs using IdentityServer4.Models; using IdentityServer4.Services; using JayCoder.MusicStore.Core.Domain.SQLEntities; using Microsoft.AspNetCore.Identity; using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; namespace JayCoder.MusicStore.Projects.IdentityServer.Profile { public class ProfileService : IProfileService { protected UserManager<ApplicationUser> _userManager; public ProfileService(UserManager<ApplicationUser> userManager) { _userManager = userManager; } public Task GetProfileDataAsync(ProfileDataRequestContext context) { var user = _userManager.GetUserAsync(context.Subject).Result; if (user != null) { var claims = new List<Claim>(); claims.Add(new Claim("FirstName", string.IsNullOrEmpty(user.FirstName) ? string.Empty : user.FirstName)); claims.Add(new Claim("LastName", string.IsNullOrEmpty(user.LastName) ? string.Empty : user.LastName)); context.IssuedClaims.AddRange(claims); } return Task.FromResult(0); } ...... } } -
Test
Login from my Microsoft account and be able to access values API
