├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── Core2AadAuth.sln ├── Core2AadAuth ├── Controllers │ ├── AccountController.cs │ └── HomeController.cs ├── Core2AadAuth.csproj ├── Extensions │ └── ClaimsPrincipalExtensions.cs ├── Filters │ └── AdalTokenAcquisitionExceptionFilter.cs ├── Models │ └── HomeMsGraphModel.cs ├── Options │ └── AuthOptions.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── AdalDistributedTokenCache.cs │ ├── ITokenCacheFactory.cs │ └── TokenCacheFactory.cs ├── Startup.cs ├── Views │ ├── Account │ │ └── SignedOut.cshtml │ ├── Home │ │ ├── Index.cshtml │ │ ├── MsGraph.cshtml │ │ ├── SignedOut.cshtml │ │ └── UserClaims.cshtml │ ├── Shared │ │ └── _Layout.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.json └── wwwroot │ └── styles.css └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #Ignore thumbnails created by Windows 3 | Thumbs.db 4 | #Ignore files built by Visual Studio 5 | *.obj 6 | *.exe 7 | *.pdb 8 | *.user 9 | *.aps 10 | *.pch 11 | *.vspscc 12 | *_i.c 13 | *_p.c 14 | *.ncb 15 | *.suo 16 | *.tlb 17 | *.tlh 18 | *.bak 19 | *.cache 20 | *.ilk 21 | *.log 22 | [Bb]in 23 | [Dd]ebug*/ 24 | *.lib 25 | *.sbr 26 | obj/ 27 | [Rr]elease*/ 28 | _ReSharper*/ 29 | [Tt]est[Rr]esult* 30 | .vs/ 31 | #Nuget packages folder 32 | packages/ 33 | *.pubxml 34 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": ".NET Core Launch (web)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceRoot}/Core2AadAuth/bin/Debug/netcoreapp2.0/Core2AadAuth.dll", 10 | "args": [], 11 | "cwd": "${workspaceRoot}/Core2AadAuth/", 12 | "stopAtEntry": false, 13 | "launchBrowser": { 14 | "enabled": true, 15 | "args": "${auto-detect-url}", 16 | "windows": { 17 | "command": "cmd.exe", 18 | "args": "/C start ${auto-detect-url}" 19 | }, 20 | "osx": { 21 | "command": "open" 22 | }, 23 | "linux": { 24 | "command": "xdg-open" 25 | } 26 | }, 27 | "env": { 28 | "ASPNETCORE_ENVIRONMENT": "Development" 29 | }, 30 | "sourceFileMap": { 31 | "/Views": "${workspaceRoot}/Views" 32 | } 33 | }, 34 | { 35 | "name": ".NET Core Attach", 36 | "type": "coreclr", 37 | "request": "attach", 38 | "processId": "${command:pickProcess}" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "dotnet", 6 | "isShellCommand": true, 7 | "args": [], 8 | "tasks": [ 9 | { 10 | "taskName": "build", 11 | "args": [ ], 12 | "isBuildCommand": true, 13 | "showOutput": "silent", 14 | "problemMatcher": "$msCompile" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /Core2AadAuth.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28705.295 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core2AadAuth", "Core2AadAuth\Core2AadAuth.csproj", "{4F5E185E-C1BB-4659-8A05-C54D8C952BC1}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{63A5926C-3F96-4362-A3B1-9EA17AA43B64}" 9 | ProjectSection(SolutionItems) = preProject 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Global 14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 15 | Debug|Any CPU = Debug|Any CPU 16 | Release|Any CPU = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 19 | {4F5E185E-C1BB-4659-8A05-C54D8C952BC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {4F5E185E-C1BB-4659-8A05-C54D8C952BC1}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {4F5E185E-C1BB-4659-8A05-C54D8C952BC1}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {4F5E185E-C1BB-4659-8A05-C54D8C952BC1}.Release|Any CPU.Build.0 = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(SolutionProperties) = preSolution 25 | HideSolutionNode = FALSE 26 | EndGlobalSection 27 | GlobalSection(ExtensibilityGlobals) = postSolution 28 | SolutionGuid = {3BB9C21E-5442-40EB-B908-FE5651D083FE} 29 | EndGlobalSection 30 | EndGlobal 31 | -------------------------------------------------------------------------------- /Core2AadAuth/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Core2AadAuth.Controllers 7 | { 8 | public class AccountController : Controller 9 | { 10 | public IActionResult SignIn() 11 | { 12 | return Challenge(new AuthenticationProperties 13 | { 14 | RedirectUri = "/" 15 | }); 16 | } 17 | 18 | public IActionResult SignOut() 19 | { 20 | return SignOut( 21 | new AuthenticationProperties 22 | { 23 | RedirectUri = "/Account/SignedOut" 24 | }, 25 | CookieAuthenticationDefaults.AuthenticationScheme, 26 | OpenIdConnectDefaults.AuthenticationScheme); 27 | } 28 | 29 | public IActionResult SignedOut() => View(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Core2AadAuth/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Net.Http.Headers; 3 | using System.Threading.Tasks; 4 | using Core2AadAuth.Extensions; 5 | using Core2AadAuth.Models; 6 | using Core2AadAuth.Options; 7 | using Core2AadAuth.Services; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.Extensions.Options; 11 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 12 | using Newtonsoft.Json; 13 | 14 | namespace Core2AadAuth.Controllers 15 | { 16 | [Authorize] 17 | public class HomeController : Controller 18 | { 19 | private static readonly HttpClient Client = new HttpClient(); 20 | private readonly ITokenCacheFactory _tokenCacheFactory; 21 | private readonly AuthOptions _authOptions; 22 | 23 | public HomeController(ITokenCacheFactory tokenCacheFactory, IOptions authOptions) 24 | { 25 | _tokenCacheFactory = tokenCacheFactory; 26 | _authOptions = authOptions.Value; 27 | } 28 | 29 | [HttpGet] 30 | [AllowAnonymous] 31 | public IActionResult Index() => View(); 32 | 33 | [HttpGet] 34 | public IActionResult UserClaims() => View(); 35 | 36 | [HttpGet] 37 | public async Task MsGraph() 38 | { 39 | HttpResponseMessage res = await QueryGraphAsync("/me"); 40 | 41 | string rawResponse = await res.Content.ReadAsStringAsync(); 42 | string prettyResponse = 43 | JsonConvert.SerializeObject(JsonConvert.DeserializeObject(rawResponse), Formatting.Indented); 44 | 45 | var model = new HomeMsGraphModel 46 | { 47 | GraphResponse = prettyResponse 48 | }; 49 | return View(model); 50 | } 51 | 52 | [HttpGet] 53 | public async Task ProfilePhoto() 54 | { 55 | HttpResponseMessage res = await QueryGraphAsync("/me/photo/$value"); 56 | 57 | return File(await res.Content.ReadAsStreamAsync(), "image/jpeg"); 58 | } 59 | 60 | //Normally this stuff would be in another service, not in the controller 61 | //But for the sake of an example, it is a bit easier 62 | private async Task QueryGraphAsync(string relativeUrl) 63 | { 64 | var req = new HttpRequestMessage(HttpMethod.Get, "https://graph.microsoft.com/beta" + relativeUrl); 65 | 66 | string accessToken = await GetAccessTokenAsync(); 67 | req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); 68 | 69 | return await Client.SendAsync(req); 70 | } 71 | 72 | private async Task GetAccessTokenAsync() 73 | { 74 | string authority = _authOptions.Authority; 75 | 76 | var cache = _tokenCacheFactory.CreateForUser(User); 77 | 78 | var authContext = new AuthenticationContext(authority, cache); 79 | 80 | //App's credentials may be needed if access tokens need to be refreshed with a refresh token 81 | string clientId = _authOptions.ClientId; 82 | string clientSecret = _authOptions.ClientSecret; 83 | var credential = new ClientCredential(clientId, clientSecret); 84 | var userId = User.GetObjectId(); 85 | 86 | var result = await authContext.AcquireTokenSilentAsync( 87 | "https://graph.microsoft.com", 88 | credential, 89 | new UserIdentifier(userId, UserIdentifierType.UniqueId)); 90 | 91 | return result.AccessToken; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Core2AadAuth/Core2AadAuth.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 04eb8c26-e801-4cfb-bf48-1d76e833b69b 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Core2AadAuth/Extensions/ClaimsPrincipalExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Claims; 3 | 4 | namespace Core2AadAuth.Extensions 5 | { 6 | public static class ClaimsPrincipalExtensions 7 | { 8 | /// 9 | /// Gets the user's Azure AD object id 10 | /// 11 | public static string GetObjectId(this ClaimsPrincipal principal) 12 | { 13 | if (principal == null) 14 | { 15 | throw new ArgumentNullException(nameof(principal)); 16 | } 17 | 18 | return principal.FindFirstValue("http://schemas.microsoft.com/identity/claims/objectidentifier"); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Core2AadAuth/Filters/AdalTokenAcquisitionExceptionFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 4 | 5 | namespace Core2AadAuth.Filters 6 | { 7 | /// 8 | /// Triggers authentication if access token cannot be acquired 9 | /// silently, i.e. from cache. 10 | /// 11 | public class AdalTokenAcquisitionExceptionFilter : ExceptionFilterAttribute 12 | { 13 | public override void OnException(ExceptionContext context) 14 | { 15 | //If ADAL failed to acquire access token 16 | if (context.Exception is AdalSilentTokenAcquisitionException) 17 | { 18 | //Send user to Azure AD to re-authenticate 19 | context.Result = new ChallengeResult(); 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Core2AadAuth/Models/HomeMsGraphModel.cs: -------------------------------------------------------------------------------- 1 | namespace Core2AadAuth.Models 2 | { 3 | public class HomeMsGraphModel 4 | { 5 | public string GraphResponse { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Core2AadAuth/Options/AuthOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Core2AadAuth.Options 2 | { 3 | public class AuthOptions 4 | { 5 | public string Authority { get; set; } 6 | public string ClientId { get; set; } 7 | public string ClientSecret { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Core2AadAuth/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace Core2AadAuth 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateWebHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Core2AadAuth/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "https://localhost:44372/", 7 | "sslPort": 44372 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Core2AadAuth": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Core2AadAuth/Services/AdalDistributedTokenCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.DataProtection; 3 | using Microsoft.Extensions.Caching.Distributed; 4 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 5 | 6 | namespace Core2AadAuth.Services 7 | { 8 | /// 9 | /// Caches access and refresh tokens for Azure AD 10 | /// 11 | public class AdalDistributedTokenCache : TokenCache 12 | { 13 | private readonly IDistributedCache _distributedCache; 14 | private readonly IDataProtector _dataProtector; 15 | private readonly string _userId; 16 | 17 | /// 18 | /// Constructs a token cache 19 | /// 20 | /// Distributed cache used for storing tokens 21 | /// The protector provider for encrypting/decrypting the cached data 22 | /// The user's unique identifier 23 | public AdalDistributedTokenCache( 24 | IDistributedCache distributedCache, 25 | IDataProtectionProvider dataProtectionProvider, 26 | string userId) 27 | { 28 | _distributedCache = distributedCache; 29 | _dataProtector = dataProtectionProvider.CreateProtector("AadTokens"); 30 | _userId = userId; 31 | BeforeAccess = BeforeAccessNotification; 32 | AfterAccess = AfterAccessNotification; 33 | } 34 | 35 | private void BeforeAccessNotification(TokenCacheNotificationArgs args) 36 | { 37 | //Called before ADAL tries to access the cache, 38 | //so this is where we should read from the distibruted cache 39 | //It sucks that ADAL's API is synchronous, so we must do a blocking call here 40 | byte[] cachedData = _distributedCache.Get(GetCacheKey()); 41 | 42 | if (cachedData != null) 43 | { 44 | //Decrypt and deserialize the cached data 45 | Deserialize(_dataProtector.Unprotect(cachedData)); 46 | } 47 | else 48 | { 49 | //Ensures the cache is cleared in TokenCache 50 | Deserialize(null); 51 | } 52 | } 53 | 54 | private void AfterAccessNotification(TokenCacheNotificationArgs args) 55 | { 56 | //Called after ADAL is done accessing the token cache 57 | if (HasStateChanged) 58 | { 59 | //In this case the cache state has changed, maybe a new token was written 60 | //So we encrypt and write the data to the distributed cache 61 | var data = _dataProtector.Protect(Serialize()); 62 | 63 | _distributedCache.Set(GetCacheKey(), data, new DistributedCacheEntryOptions 64 | { 65 | AbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1) 66 | }); 67 | 68 | HasStateChanged = false; 69 | } 70 | } 71 | 72 | private string GetCacheKey() => $"{_userId}_TokenCache"; 73 | } 74 | } -------------------------------------------------------------------------------- /Core2AadAuth/Services/ITokenCacheFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 3 | 4 | namespace Core2AadAuth.Services 5 | { 6 | public interface ITokenCacheFactory 7 | { 8 | TokenCache CreateForUser(ClaimsPrincipal user); 9 | } 10 | } -------------------------------------------------------------------------------- /Core2AadAuth/Services/TokenCacheFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Claims; 3 | using Core2AadAuth.Extensions; 4 | using Microsoft.AspNetCore.DataProtection; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 7 | 8 | namespace Core2AadAuth.Services 9 | { 10 | /// 11 | /// Responsible for creating ADAL token caches for users 12 | /// 13 | public class TokenCacheFactory : ITokenCacheFactory 14 | { 15 | private readonly IDistributedCache _distributedCache; 16 | private readonly IDataProtectionProvider _dataProtectionProvider; 17 | //Token cache is cached in-memory in this instance to avoid loading data multiple times during the request 18 | //For this reason this factory should always be registered as Scoped 19 | private TokenCache _cachedTokenCache; 20 | private string _cachedTokenCacheUserId; 21 | 22 | public TokenCacheFactory(IDistributedCache distributedCache, IDataProtectionProvider dataProtectionProvider) 23 | { 24 | _distributedCache = distributedCache; 25 | _dataProtectionProvider = dataProtectionProvider; 26 | } 27 | 28 | public TokenCache CreateForUser(ClaimsPrincipal user) 29 | { 30 | string userId = user.GetObjectId(); 31 | 32 | if (_cachedTokenCache != null) 33 | { 34 | // Guard for accidental re-use across requests 35 | if (userId != _cachedTokenCacheUserId) 36 | { 37 | throw new Exception("The cached token cache is for a different user! Make sure the token cache factory is registered as Scoped!"); 38 | } 39 | 40 | return _cachedTokenCache; 41 | } 42 | 43 | _cachedTokenCache = new AdalDistributedTokenCache( 44 | _distributedCache, _dataProtectionProvider, userId); 45 | _cachedTokenCacheUserId = userId; 46 | return _cachedTokenCache; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Core2AadAuth/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Core2AadAuth.Filters; 3 | using Core2AadAuth.Options; 4 | using Core2AadAuth.Services; 5 | using Microsoft.AspNetCore.Authentication.Cookies; 6 | using Microsoft.AspNetCore.Authentication.OpenIdConnect; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Http.Extensions; 11 | using Microsoft.AspNetCore.HttpsPolicy; 12 | using Microsoft.AspNetCore.Mvc; 13 | using Microsoft.Extensions.Configuration; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.IdentityModel.Clients.ActiveDirectory; 16 | 17 | namespace Core2AadAuth 18 | { 19 | public class Startup 20 | { 21 | public Startup(IConfiguration configuration) 22 | { 23 | Configuration = configuration; 24 | } 25 | 26 | private IConfiguration Configuration { get; } 27 | 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddMvc(opts => 31 | { 32 | opts.Filters.Add(typeof(AdalTokenAcquisitionExceptionFilter)); 33 | }).SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 34 | 35 | //TODO: Set up Data Protection key persistence correctly for your env: https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?tabs=aspnetcore2x 36 | //I go with defaults, which works fine in my case 37 | //But if you run on Azure App Service and use deployment slots, keys get swapped with the app 38 | //So you'll need to setup storage for keys outside the app, Key Vault and Blob Storage are some options 39 | services.AddDataProtection(); 40 | 41 | //Add a strongly-typed options class to DI 42 | services.Configure(Configuration.GetSection("Authentication")); 43 | 44 | services.AddScoped(); 45 | 46 | services.AddAuthentication(auth => 47 | { 48 | auth.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 49 | auth.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; 50 | }) 51 | .AddCookie() 52 | .AddOpenIdConnect(opts => 53 | { 54 | Configuration.GetSection("Authentication").Bind(opts); 55 | 56 | opts.Events = new OpenIdConnectEvents 57 | { 58 | OnAuthorizationCodeReceived = async ctx => 59 | { 60 | HttpRequest request = ctx.HttpContext.Request; 61 | //We need to also specify the redirect URL used 62 | string currentUri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path); 63 | //Credentials for app itself 64 | var credential = new ClientCredential(ctx.Options.ClientId, ctx.Options.ClientSecret); 65 | 66 | //Construct token cache 67 | ITokenCacheFactory cacheFactory = ctx.HttpContext.RequestServices.GetRequiredService(); 68 | TokenCache cache = cacheFactory.CreateForUser(ctx.Principal); 69 | 70 | var authContext = new AuthenticationContext(ctx.Options.Authority, cache); 71 | 72 | //Get token for Microsoft Graph API using the authorization code 73 | string resource = "https://graph.microsoft.com"; 74 | AuthenticationResult result = await authContext.AcquireTokenByAuthorizationCodeAsync( 75 | ctx.ProtocolMessage.Code, new Uri(currentUri), credential, resource); 76 | 77 | //Tell the OIDC middleware we got the tokens, it doesn't need to do anything 78 | ctx.HandleCodeRedemption(result.AccessToken, result.IdToken); 79 | } 80 | }; 81 | }); 82 | 83 | services.Configure(o => 84 | { 85 | o.IncludeSubDomains = false; 86 | o.Preload = false; 87 | o.MaxAge = TimeSpan.FromDays(365); 88 | }); 89 | } 90 | 91 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 92 | { 93 | if (env.IsDevelopment()) 94 | { 95 | app.UseDeveloperExceptionPage(); 96 | } 97 | else 98 | { 99 | //Outside dev, require HTTPS and use HSTS 100 | app.UseHttpsRedirection(); 101 | app.UseHsts(); 102 | } 103 | 104 | app.UseStaticFiles(); 105 | 106 | app.UseAuthentication(); 107 | 108 | app.UseMvcWithDefaultRoute(); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Core2AadAuth/Views/Account/SignedOut.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Signed out"; 3 | } 4 | 5 |

Signed out successfully

-------------------------------------------------------------------------------- /Core2AadAuth/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Index"; 3 | } 4 | 5 |

Welcome

-------------------------------------------------------------------------------- /Core2AadAuth/Views/Home/MsGraph.cshtml: -------------------------------------------------------------------------------- 1 | @model HomeMsGraphModel 2 | @{ 3 | ViewData["Title"] = "MS Graph test"; 4 | } 5 | 6 |
7 |
8 |

Response from Graph:

9 |
@Model.GraphResponse
10 |
11 |
12 |

User photo

13 | 14 |
15 |
-------------------------------------------------------------------------------- /Core2AadAuth/Views/Home/SignedOut.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Signed out"; 3 | } 4 |

Signed out

-------------------------------------------------------------------------------- /Core2AadAuth/Views/Home/UserClaims.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "User claims"; 3 | } 4 | 5 |

User claims

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | @foreach(Claim claim in User.Claims) 16 | { 17 | 18 | 19 | 20 | 21 | } 22 | 23 |
TypeValue
@claim.Type@claim.Value
-------------------------------------------------------------------------------- /Core2AadAuth/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - AAD Auth tester 7 | 8 | 12 | 13 | 14 | 15 | 16 | 46 |
47 | @RenderBody() 48 |
49 | 50 | 53 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Core2AadAuth/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Core2AadAuth.Models 2 | @using System.Security.Claims 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -------------------------------------------------------------------------------- /Core2AadAuth/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /Core2AadAuth/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Authentication": { 3 | "ClientId": "put-your-client-id-here", 4 | "Authority": "https://login.microsoftonline.com/your-tenant-id/", 5 | "PostLogoutRedirectUri": "http://localhost:5000", 6 | "CallbackPath": "/signin-oidc", 7 | "ResponseType": "code id_token" 8 | } 9 | } -------------------------------------------------------------------------------- /Core2AadAuth/wwwroot/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top:70px; 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core 2.1 Azure AD authentication example 2 | 3 | This sample application is built on ASP.NET Core 2.1 to test authentication via Azure AD. 4 | 5 | ## Pre-requisites 6 | 7 | You will need a development environment capable of running an ASP.NET Core 2.1 application. 8 | 9 | Windows users can install [Visual Studio 2017](https://www.visualstudio.com/downloads/) with the **ASP.NET and web development workload**. 10 | 11 | Users on Windows, Mac, or Linux can download the [.NET Core SDK](https://www.microsoft.com/net/download) and use any editor that works best. 12 | [Visual Studio Code](https://code.visualstudio.com/) is pretty good. 13 | 14 | ## Setup instructions 15 | 16 | To run the app locally, you'll need to register an application in Azure AD. 17 | 18 | How to register the app: 19 | 20 | 1. Go to [https://portal.azure.com](https://portal.azure.com) 21 | 1. Find *Azure Active Directory* on the left or from under *All services* 22 | 1. Go to *App registrations* 23 | 1. Click on *New application registration* 24 | 1. Give the app a name, e.g. **ASP.NET Core 2 Azure AD Test** 25 | 1. Make sure the application type is **Web app/API** 26 | 1. Set sign-on URL to **http://localhost:5000/Account/SignIn** 27 | 1. Click **Create** 28 | 29 | Getting client id, setting reply URL, and generating client secret: 30 | 31 | 1. After creation, open the app 32 | 1. Copy the **Application ID**, and put it somewhere, this is also called the Client ID 33 | 1. Click **Settings** and then **Reply URLs** 34 | 1. Add **https://localhost:5000/signin-oidc** to the list and save it 35 | 1. Go to **Keys** 36 | 1. In the *Passwords* section, put some description for the key, select the expiry, and hit **Save** 37 | 1. Copy the key value somewhere, this is your client secret (keep it secret) 38 | 39 | Adding permissions for Microsoft Graph API: 40 | 41 | 1. Find your app in the Azure AD blade's App Registrations tab in Azure Portal 42 | 1. Go to Required permissions 43 | 1. Click Add 44 | 1. Choose *Microsoft Graph* as the API 45 | 1. Select *Sign in and read user profile*, *View users' basic profile*, and *View users' email address* under *Delegated permissions* 46 | 1. Click Select and Done 47 | 48 | Getting the authority URL: 49 | 50 | 1. Go back to the App registrations list 51 | 1. Click **Endpoints** 52 | 1. Copy the **OAuth 2.0 Authorization Endpoint** value 53 | 1. Remove the */oauth2/authorize* part from the URL, the result is your *Authority* 54 | 55 | Fill the values in settings: 56 | 57 | 1. Open the solution in Visual Studio 58 | 1. Set client id and authority in appsettings.json 59 | 1. Right-click on the project and click **Manage user secrets** 60 | 1. Add the client secret here. Example below: 61 | 62 | ```json 63 | { 64 | "Authentication":{ 65 | "ClientSecret": "your-client-secret....." 66 | } 67 | } 68 | ``` 69 | 70 | The main reason to put the client secret there is to make sure it is not accidentally put into version control. 71 | This is not absolute advice and you must make the decision how to store configurations for your app. 72 | --------------------------------------------------------------------------------