Skip to content

AspNetCore OAuth sample #61

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions NetCoreOAuthWebSample/AzureDevOpsOAuthTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Server.IIS.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Threading.Tasks;

namespace NetCoreOAuthWebSample
{
public class AzureDevOpsOAuthOptions : OAuthOptions
{
public AzureDevOpsOAuthOptions()
{
ClaimsIssuer = AzureDevOpsAuthenticationDefaults.Issuer;
CallbackPath = AzureDevOpsAuthenticationDefaults.CallbackPath;
AuthorizationEndpoint = AzureDevOpsAuthenticationDefaults.AuthorizationEndPoint;
TokenEndpoint = AzureDevOpsAuthenticationDefaults.TokenEndPoint;
UserInformationEndpoint = AzureDevOpsAuthenticationDefaults.UserInformationEndPoint;

ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
ClaimActions.MapJsonKey(ClaimTypes.Email, "emailAddress");
ClaimActions.MapJsonKey(ClaimTypes.Name, "displayName");
}
}

public static class AzureDevOpsAuthenticationDefaults
{
public const string AuthenticationScheme = "AzureDevOps";
public const string Display = "AzureDevOps";
public const string Issuer = "AzureDevOps";
public const string CallbackPath = "/signin-azdo";
public const string AuthorizationEndPoint = "https://app.vssps.visualstudio.com/oauth2/authorize";
public const string TokenEndPoint = "https://app.vssps.visualstudio.com/oauth2/token";
public const string UserInformationEndPoint = "https://app.vssps.visualstudio.com/_apis/profile/profiles/me";
}

public static class AzureDevOpsExtensions
{
public static AuthenticationBuilder AddAzureDevOps(this AuthenticationBuilder builder, Action<AzureDevOpsOAuthOptions> configuration) =>
builder.AddOAuth<AzureDevOpsOAuthOptions, AzureDevOpsAuthenticationHandler>(
AzureDevOpsAuthenticationDefaults.AuthenticationScheme,
AzureDevOpsAuthenticationDefaults.Display,
configuration);
}

public class AzureDevOpsAuthenticationHandler : OAuthHandler<AzureDevOpsOAuthOptions>
{
public AzureDevOpsAuthenticationHandler(
IOptionsMonitor<AzureDevOpsOAuthOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock)
: base(options, logger, encoder, clock)
{

}

protected override async Task<AuthenticationTicket> CreateTicketAsync(
ClaimsIdentity identity,
AuthenticationProperties properties,
OAuthTokenResponse tokens)
{
using var request = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);

using var response = await Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, Context.RequestAborted);
if (!response.IsSuccessStatusCode)
{
Logger.LogError("An error occurred while retrieving the user profile: the remote server " +
"returned a {Status} response with the following payload: {Headers} {Body}.",
response.StatusCode,
response.Headers.ToString(),
await response.Content.ReadAsStringAsync());

throw new HttpRequestException("An error occurred while retrieving the user profile.");
}

using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
var principal = new ClaimsPrincipal(identity);
var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);
context.RunClaimActions();

await Options.Events.CreatingTicket(context);
return new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name);
}

protected override async Task<OAuthTokenResponse> ExchangeCodeAsync(OAuthCodeExchangeContext context)
{
using var request = new HttpRequestMessage(HttpMethod.Post, Options.TokenEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

request.Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
["redirect_uri"] = context.RedirectUri,
["client_assertion"] = Options.ClientSecret,
["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
["assertion"] = context.Code,
["grant_type"] = "urn:ietf:params:oauth:grant-type:jwt-bearer",
});

using var response = await Backchannel.SendAsync(request, Context.RequestAborted);

if (!response.IsSuccessStatusCode)
{
Logger.LogError("An error occurred while retrieving an access token: the remote server " +
"returned a {Status} response with the following payload: {Headers} {Body}.",
response.StatusCode,
response.Headers.ToString(),
await response.Content.ReadAsStringAsync());

return OAuthTokenResponse.Failed(new Exception("An error occurred while retrieving an access token."));
}

var content = await response.Content.ReadAsStringAsync();
var payload = JsonDocument.Parse(content);

return OAuthTokenResponse.Success(payload);
}
}
}
19 changes: 19 additions & 0 deletions NetCoreOAuthWebSample/Controllers/AuthenticationController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Mvc;

namespace NetCoreOAuthWebSample.Controllers
{
public class AuthenticationController : Controller
{
[HttpPost("~/signin")]
public IActionResult SignIn([FromForm] string provider) =>
Challenge(new AuthenticationProperties { RedirectUri = "/" });

[HttpPost("~/signout")]
public IActionResult SignOut() =>
SignOut(new AuthenticationProperties { RedirectUri = "/" },
CookieAuthenticationDefaults.AuthenticationScheme);
}
}
8 changes: 8 additions & 0 deletions NetCoreOAuthWebSample/NetCoreOAuthWebSample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>b1e36ef6-1e2f-41bc-a349-e2cf05a9e820</UserSecretsId>
</PropertyGroup>

</Project>
26 changes: 26 additions & 0 deletions NetCoreOAuthWebSample/Pages/Error.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@page
@model ErrorModel
@{
ViewData["Title"] = "Error";
}

<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>

@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}

<h3>Development Mode</h3>
<p>
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
31 changes: 31 additions & 0 deletions NetCoreOAuthWebSample/Pages/Error.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace NetCoreOAuthWebSample.Pages
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
public string RequestId { get; set; }

public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

private readonly ILogger<ErrorModel> _logger;

public ErrorModel(ILogger<ErrorModel> logger)
{
_logger = logger;
}

public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}
}
28 changes: 28 additions & 0 deletions NetCoreOAuthWebSample/Pages/Index.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}

@if (User?.Identity?.IsAuthenticated ?? false)
{
<h1>Welcome, @User.Identity.Name</h1>

<p>
@foreach (var claim in @Model.HttpContext.User.Claims)
{
<div><code>@claim.Type</code>: <strong>@claim.Value</strong></div>
}
</p>
<form action="/signout" method="post">
<button class="btn btn-lg btn-success m-1" type="submit">Sign Out</button>
</form>
}
else
{
<h1>Welcome, anonymous</h1>
<form action="/signin" method="post">
<button class="btn btn-lg btn-success m-1" type="submit">Sign In</button>
</form>
}

25 changes: 25 additions & 0 deletions NetCoreOAuthWebSample/Pages/Index.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace NetCoreOAuthWebSample.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;

public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}

public void OnGet()
{

}
}
}
8 changes: 8 additions & 0 deletions NetCoreOAuthWebSample/Pages/Privacy.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@page
@model PrivacyModel
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>
24 changes: 24 additions & 0 deletions NetCoreOAuthWebSample/Pages/Privacy.cshtml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace NetCoreOAuthWebSample.Pages
{
public class PrivacyModel : PageModel
{
private readonly ILogger<PrivacyModel> _logger;

public PrivacyModel(ILogger<PrivacyModel> logger)
{
_logger = logger;
}

public void OnGet()
{
}
}
}
50 changes: 50 additions & 0 deletions NetCoreOAuthWebSample/Pages/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - NetCoreOAuthWebSample</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-page="/Index">NetCoreOAuthWebSample</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">
<div class="container">
&copy; 2020 - NetCoreOAuthWebSample - <a asp-area="" asp-page="/Privacy">Privacy</a>
</div>
</footer>

<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>

@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
3 changes: 3 additions & 0 deletions NetCoreOAuthWebSample/Pages/_ViewImports.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@using NetCoreOAuthWebSample
@namespace NetCoreOAuthWebSample.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
3 changes: 3 additions & 0 deletions NetCoreOAuthWebSample/Pages/_ViewStart.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}
26 changes: 26 additions & 0 deletions NetCoreOAuthWebSample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace NetCoreOAuthWebSample
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Loading