.NET Framework 4.5+ MVC OpenID Connect example
The standard library for OpenID Connect published by the Microsoft for .NET Framework 4 is not compatible with OpenAthens Keystone. OpenAthens has published an updated library that makes it possible to connect to OpenAthens Keystone in .NET 4.5 or later. Earlier versions are not supported.
It is assumed that user has knowledge of developing applications using ASP.NET in Visual Studio. This example is based on the ASP.NET MVC Web Application template.
A completed sample web application created using these instructions is available at https://github.com/openathens/KeystoneConnectorDotNet4Sample - to use this:
- Check out the above repository
- Create a new Application record in the OpenAthens Service Provider dashboard, using
https://localhost:44328
as the Application URL (with no trailing /). Copy the same URL (including the port number) to the Redirect URL (including trailing /). - Update the appSettings in Web.config using the Client Id and Client Secret of the application record created above.
Goal in this example
Authenticate a user and display all the received claims on a page. In the real world you would read the claims and feed them into your authorisation / user-session management process.
Instructions
Create project
Create a new project in Visual Studio, selecting the template “ASP.NET Web Application (.NET Framework)” - select framework version 4.5 or higher. At the second stage, select the MVC template option. Don’t select any authentication options at this stage.
Configure the website to run on https by default:
- In the solution explorer, select the project folder, then in the properties pane, change SSL Enabled to true. Copy the SSL URL that is created.
- Right click the project and select properties, and in the Web tab, paste the https copied above into the Project Url field in place of the existing http URL.
Create a new Application record in the OpenAthens Service Provider dashboard, using the URL you copied in step 1 as the Application URL (with no trailing /). Copy the same URL (including the port number) to the Redirect URL.
Add the dependencies
Use NuGet package manager to add the following packages:
Microsoft.AspNet.Identity.Owin
Microsoft.Owin.Host.SystemWeb
OpenAthens.Owin.Security.OpenIdConnect
Configuration
Add the following appSettings to Web.config:
<add key="oidc:Authority" value="https://connect.openathens.net" />
<add key="oidc:ClientId" value="{clientId from the OIDC application record created in the publisher dashboard}" />
<add key="oidc:ClientSecret" value="{clientSecret from the OIDC application created in the publisher dashboard}" />
<add key="oidc:RedirectUrl" value="{redirectUrl set in the publisher dashboard}" />
For added security to avoid accidentally checking secure strings into source control, you could place these settings in a separate config file outside the project root and include a reference to, e.g.:
<appSettings file="..\..\EnvironmentSettings.config">
<add … />
</appSetting>
Add a Startup class that initialises the OpenID Connect authentication via an Owin assembly binding. Following the convention used in other samples from Microsoft, this is a partial class split across two files. Firstly, Startup.cs in the project root:
using Microsoft.Owin;
using Owin;
[assembly: OwinStartup(typeof(WebApplication1.Startup))]
namespace WebApplication1
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
}
}
and secondly, Startup.Auth.cs in the App_Start folder. (you’ll need to remove “.App_Start” from the namespace in the created file so it matches the namespace in Startup.cs):
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using OpenAthens.Owin.Security.OpenIdConnect;
using Owin;
using System.Configuration;
namespace WebApplication1
{
public partial class Startup
{
public static string OidcAuthority = ConfigurationManager.AppSettings["oidc:Authority"];
public static string OidcRedirectUrl = ConfigurationManager.AppSettings["oidc:RedirectUrl"];
public static string OidcClientId = ConfigurationManager.AppSettings["oidc:ClientId"];
public static string OidcClientSecret = ConfigurationManager.AppSettings["oidc:ClientSecret"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
var oidcOptions = new OpenIdConnectAuthenticationOptions
{
Authority = OidcAuthority,
ClientId = OidcClientId,
ClientSecret = OidcClientSecret,
GetClaimsFromUserInfoEndpoint = true,
PostLogoutRedirectUri = OidcRedirectUrl,
RedirectUri = OidcRedirectUrl,
ResponseType = OpenIdConnectResponseType.Code,
Scope = OpenIdConnectScope.OpenId
};
app.UseOpenIdConnectAuthentication(oidcOptions);
}
}
}
Configure global security settings
Add settings to Application_Start in Global.asax.cs for the anti-forgery token identifier. The ServicePointManager line is not needed if you're using version 4.6 or later:
using System.IdentityModel.Claims;
using System.Web.Helpers;
…
protected void Application_Start()
{
…
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
}
…
Add login and claims links:
Add the _LoginPartial.cshtml to the Views\Shared folder, to display a Login link if a user session does not exist and will display links to logout and view the returned claims if a user session does:
@if (Request.IsAuthenticated)
{
using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" }))
{
@Html.AntiForgeryToken()
<ul class="nav navbar-nav navbar-right">
<li>
@Html.ActionLink("Claims", "Claims", "Account", routeValues: null, htmlAttributes: new { title = "Claims" })
</li>
<li><a href="javascript:document.getElementById('logoutForm').submit()">Log off</a></li>
</ul>
}
}
else
{
<ul class="nav navbar-nav navbar-right">
<li>@Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
</ul>
}
Reference this in _Layout.cshtml after the main nav bar:
…
<ul class="nav navbar-nav">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
</ul>
@Html.Partial("_LoginPartial")
…
Create claims page
Create a new folder in Views called Account. Add a new partial view called Claims and paste in the following code which will display all claims that have been returned:
@using System.Security.Claims
@{
ViewBag.Title = "Claims";
}
<h2>@ViewBag.Title</h2>
<h4>Claims Present in the Claims Identity:</h4>
<table class="table table-striped table-bordered table-hover table-condensed claim-table">
<tr>
<th class="claim-type claim-data claim-head">Claim Type</th>
<th class="claim-data claim-head">Claim Value Type</th>
<th class="claim-data claim-head">Claim Value</th>
</tr>
@foreach (Claim claim in ClaimsPrincipal.Current.Claims)
{
<tr>
<td class="claim-type claim-data">@claim.Type</td>
<td class="claim-data">@claim.ValueType</td>
<td class="claim-data">@claim.Value</td>
</tr>
}
</table>
Create AccountController.cs in the Controllers folder:
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace WebApplication1.Controllers
{
public class AccountController : Controller
{
// GET: Login
public void Login(string returnUrl = "/")
{
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge();
return;
}
Response.Redirect("/");
}
public void LogOff()
{
if (Request.IsAuthenticated)
{
var authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
}
Response.Redirect("/");
}
[Authorize]
public ActionResult Claims()
{
return View();
}
}
}
Example of accessing the attributes
An example of accessing the OpenAthens scoped affiliation claim is:
var personScopedAffiliation = (User.Identity as ClaimsIdentity)?.Claims
.FirstOrDefault(c => c.Type == "eduPersonScopedAffiliation")?.Value;
You can then use this to make authorisation decisions.