Skip to content

Instantly share code, notes, and snippets.

@ilmax
Created September 30, 2015 17:02
Show Gist options
  • Save ilmax/a449adcfbb0f1b16dff0 to your computer and use it in GitHub Desktop.
Save ilmax/a449adcfbb0f1b16dff0 to your computer and use it in GitHub Desktop.
public class AuthorizationController : Controller
{
[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post)]
[Route("~/connect/authorize")]
public async Task<ActionResult> Authorize(CancellationToken cancellationToken)
{
// Note: when a fatal error occurs during the request processing, an OpenID Connect response
// is prematurely forged and added to the OWIN context by OpenIdConnectServerHandler.
// In this case, the OpenID Connect request is null and cannot be used.
// When the user agent can be safely redirected to the client application,
// OpenIdConnectServerHandler automatically handles the error and MVC is not invoked.
// You can safely remove this part and let Owin.Security.OpenIdConnect.Server automatically
// handle the unrecoverable errors by switching ApplicationCanDisplayErrors to false in Startup.cs
var response = OwinContext.GetOpenIdConnectResponse();
if (response != null)
{
return View("Error", response);
}
// Extract the authorization request from the cache, the query string or the request form.
var request = OwinContext.GetOpenIdConnectRequest();
if (request == null)
{
return View("Error", new OpenIdConnectMessage
{
Error = "invalid_request",
ErrorDescription = "An internal error has occurred"
});
}
// Note: authentication could be theorically enforced at the filter level via AuthorizeAttribute
// but this authorization endpoint accepts both GET and POST requests while the cookie middleware
// only uses 302 responses to redirect the user agent to the login page, making it incompatible with POST.
// To work around this limitation, the OpenID Connect request is saved in the cache and will be
// restored in the other "Authorize" method, after the authentication process has been completed.
if (User.Identity == null || !User.Identity.IsAuthenticated)
{
return View("Login", new LoginViewModel(OwinContext.Authentication.GetExternalProviders()));
//return RedirectToAction("SignIn", "Authentication", new {
// returnUrl = Url.Action("Authorize", new {
// unique_id = request.GetUniqueIdentifier()
// })
//});
}
// Note: Owin.Security.OpenIdConnect.Server automatically ensures an application
// corresponds to the client_id specified in the authorization request using
// IOpenIdConnectServerProvider.ValidateClientRedirectUri (see AuthorizationProvider.cs).
// In theory, this null check is thus not strictly necessary. That said, a race condition
// and a null reference exception could appear here if you manually removed the application
// details from the database after the initial check made by Owin.Security.OpenIdConnect.Server.
var application = await GetApplicationAsync(request.ClientId, cancellationToken);
if (application == null)
{
return View("Error", new OpenIdConnectMessage
{
Error = "invalid_client",
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
// Note: in a real world application, you'd probably prefer creating a specific view model.
return View("Authorize", Tuple.Create(request, application));
}
[Authorize, HttpPost, Route("~/connect/authorize/accept"), ValidateAntiForgeryToken]
public async Task<ActionResult> Accept(CancellationToken cancellationToken)
{
// Extract the authorization request from the cache, the query string or the request form.
var request = OwinContext.GetOpenIdConnectRequest();
if (request == null)
{
return View("Error", new OpenIdConnectMessage
{
Error = "invalid_request",
ErrorDescription = "An internal error has occurred"
});
}
// Create a new ClaimsIdentity containing the claims that
// will be used to create an id_token, a token or a code.
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationType);
foreach (var claim in OwinContext.Authentication.User.Claims)
{
// Allow ClaimTypes.Name to be added in the id_token.
// ClaimTypes.NameIdentifier is automatically added, even if its
// destination is not defined or doesn't include "id_token".
// The other claims won't be visible for the client application.
if (claim.Type == ClaimTypes.Name)
{
claim.WithDestination("id_token")
.WithDestination("token");
}
identity.AddClaim(claim);
}
// Note: Owin.Security.OpenIdConnect.Server automatically ensures an application
// corresponds to the client_id specified in the authorization request using
// IOpenIdConnectServerProvider.ValidateClientRedirectUri (see AuthorizationProvider.cs).
// In theory, this null check is thus not strictly necessary. That said, a race condition
// and a null reference exception could appear here if you manually removed the application
// details from the database after the initial check made by Owin.Security.OpenIdConnect.Server.
var application = await GetApplicationAsync(request.ClientId, CancellationToken.None);
if (application == null)
{
return View("Error", new OpenIdConnectMessage
{
Error = "invalid_client",
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
// Create a new ClaimsIdentity containing the claims associated with the application.
// Note: setting identity.Actor is not mandatory but can be useful to access
// the whole delegation chain from the resource server (see ResourceController.cs).
identity.Actor = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationType);
identity.Actor.AddClaim(ClaimTypes.NameIdentifier, application.ApplicationID);
identity.Actor.AddClaim(ClaimTypes.Name, application.DisplayName, destination: "id_token token");
// This call will instruct Owin.Security.OpenIdConnect.Server to serialize
// the specified identity to build appropriate tokens (id_token and token).
// Note: you should always make sure the identities you return contain either
// a 'sub' or a 'ClaimTypes.NameIdentifier' claim. In this case, the returned
// identities always contain the name identifier returned by the external provider.
OwinContext.Authentication.SignIn(identity);
return new HttpStatusCodeResult(200);
}
[Authorize, HttpPost, Route("~/connect/authorize/deny"), ValidateAntiForgeryToken]
public ActionResult Deny(CancellationToken cancellationToken)
{
// Extract the authorization request from the cache, the query string or the request form.
var request = OwinContext.GetOpenIdConnectRequest();
if (request == null)
{
return View("Error", new OpenIdConnectMessage
{
Error = "invalid_request",
ErrorDescription = "An internal error has occurred"
});
}
// Notify Owin.Security.OpenIdConnect.Server that the authorization grant has been denied.
// Note: OpenIdConnectServerHandler will automatically take care of redirecting
// the user agent to the client application using the appropriate response_mode.
OwinContext.SetOpenIdConnectResponse(new OpenIdConnectMessage
{
Error = "access_denied",
ErrorDescription = "The authorization grant has been denied by the resource owner",
RedirectUri = request.RedirectUri,
State = request.State
});
return new HttpStatusCodeResult(200);
}
[HttpGet, Route("~/connect/logout")]
public async Task<ActionResult> Logout()
{
// Note: when a fatal error occurs during the request processing, an OpenID Connect response
// is prematurely forged and added to the OWIN context by OpenIdConnectServerHandler.
// In this case, the OpenID Connect request is null and cannot be used.
// When the user agent can be safely redirected to the client application,
// OpenIdConnectServerHandler automatically handles the error and MVC is not invoked.
// You can safely remove this part and let Owin.Security.OpenIdConnect.Server automatically
// handle the unrecoverable errors by switching ApplicationCanDisplayErrors to false in Startup.cs
var response = OwinContext.GetOpenIdConnectResponse();
if (response != null)
{
return View("Error", response);
}
// When invoked, the logout endpoint might receive an unauthenticated request if the server cookie has expired.
// When the client application sends an id_token_hint parameter, the corresponding identity can be retrieved
// using AuthenticateAsync or using User when the authorization server is declared as AuthenticationMode.Active.
var identity = await OwinContext.Authentication.AuthenticateAsync(OpenIdConnectServerDefaults.AuthenticationType);
// Extract the logout request from the OWIN environment.
var request = OwinContext.GetOpenIdConnectRequest();
if (request == null)
{
return View("Error", new OpenIdConnectMessage
{
Error = "invalid_request",
ErrorDescription = "An internal error has occurred"
});
}
return View("Logout", Tuple.Create(request, identity));
}
[HttpPost, Route("~/connect/logout")]
[ValidateAntiForgeryToken]
public ActionResult Logout(CancellationToken cancellationToken)
{
// Instruct the cookies middleware to delete the local cookie created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
OwinContext.Authentication.SignOut("ServerCookie");
// This call will instruct Owin.Security.OpenIdConnect.Server to serialize
// the specified identity to build appropriate tokens (id_token and token).
// Note: you should always make sure the identities you return contain either
// a 'sub' or a 'ClaimTypes.NameIdentifier' claim. In this case, the returned
// identities always contain the name identifier returned by the external provider.
OwinContext.Authentication.SignOut(OpenIdConnectServerDefaults.AuthenticationType);
return new HttpStatusCodeResult(200);
}
[HttpPost, Route("~/connect/authorize/login"), ValidateAntiForgeryToken]
public ActionResult LoginWithPassword(LoginModel login, string queryString)
{
Debug.Assert(!User.Identity.IsAuthenticated);
if (login.UserName == login.Password)
{
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, login.UserName).WithDestination("id_token token"));
OwinContext.Authentication.SignIn(identity);
return new HttpStatusCodeResult(200);
}
return View("Login", new LoginViewModel(OwinContext.Authentication.GetExternalProviders(), "Wrong password"));
}
/// <summary>
/// Gets the IOwinContext instance associated with the current request.
/// </summary>
protected IOwinContext OwinContext
{
get
{
var context = HttpContext.GetOwinContext();
if (context == null)
{
throw new NotSupportedException("An OWIN context cannot be extracted from HttpContext");
}
return context;
}
}
protected virtual async Task<Application> GetApplicationAsync(string identifier, CancellationToken cancellationToken)
{
using (var context = new ApplicationContext())
{
// Retrieve the application details corresponding to the requested client_id.
return await (from application in context.Applications
where application.ApplicationID == identifier
select application).SingleOrDefaultAsync(cancellationToken);
}
}
}
<div class="jumbotron">
<h2>Login</h2>
<h1>Authorization Server</h1>
@if (!string.IsNullOrEmpty(Model.Error))
{
<h1 style="color:red">@Model.Error</h1>
}
@{
var action = string.Format("/connect/authorize/login?{0}", Request.QueryString);
}
<form action="@action" enctype="application/x-www-form-urlencoded" method="post">
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Forms Login</h4>
<hr />
<input type="hidden" name="queryString" value="@Request.QueryString" />
<div class="form-group">
<div class="col-md-10">
<label for="UserName">User name: </label>
<input type="text" id="UserName" name="username" />
</div>
</div>
<div class="form-group">
<div class="col-md-10">
<label for="Password">Password: </label>
<input type="password" id="Password" name="password" />
</div>
</div>
<div class="form-group">
<div class="col-md-10">
<label for="RememberMe">Remember Me</label>
<input type="checkbox" id="RememberMe" name="isPersistent" />
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Sign In" class="btn btn-default" />
</div>
</div>
</div>
</form>
@foreach (var description in Model.Providers)
{
<form action="/signin" method="post">
<input type="hidden" name="Provider" value="@description.AuthenticationType" />
<input type="hidden" name="ReturnUrl" value="@ViewBag.ReturnUrl" />
<button class="btn btn-lg btn-success" type="submit">Connect using @description.Caption</button>
</form>
}
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment