title: How to implement OpenID OAuth2 Server in ASP.NET Framework 4.x Web API?
body: <p>I'm trying to implement OpenID OAuth 2.0 server with ASP.NET Framework 4.7.2 Web API. It will be used to protect resource APIs with JWT Access/Refresh tokens. </p>
  
  <p>I'm fairly new to OpenID and OAuth so I'm looking for advice/guidelines/libray that I can use to implement this Authorization server. </p>
  
  <p>The Auth server needs to be implemented by using ASP.NET Framework 4.7.2, there is no option for Core at the moment. The resource APIs will be written in ASP.NET Core 2.X.</p>
  
  <p>I've followed the awesome Taiseer's tutorials (<a href="http://bitoftech.net/2014/06/01/token-based-authentication-asp-net-web-api-2-owin-asp-net-identity/" rel="nofollow noreferrer">Part 1</a>, <a href="http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/" rel="nofollow noreferrer">Part 3</a>, <a href="http://bitoftech.net/2014/09/24/decouple-owin-authorization-server-resource-server-oauth-2-0-web-api/" rel="nofollow noreferrer">Part 5</a>, and <a href="http://bitoftech.net/2014/10/27/json-web-token-asp-net-web-api-2-jwt-owin-authorization-server/comment-page-1/" rel="nofollow noreferrer">JWT Setup</a>), and currently have OAuth server which can generate JWT Tokens, and Core API which can validate the token.</p>
  
  <p>Here is the code that I currently have.</p>
  
  <p>Auth Server: </p>
  
  <p>Startup.cs</p>
  
  <pre><code>public class Startup
  {
      public void Configuration(IAppBuilder app)
      {
          ConfigureOAuth(app);
  
          HttpConfiguration config = new HttpConfiguration();
          WebApiConfig.Register(config);
          app.UseWebApi(config);
      }
  
      public void ConfigureOAuth(IAppBuilder app)
      {
          app.CreatePerOwinContext&lt;SecurityUserManager&gt;(SecurityUserManager.Create);
  
          OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
          {
              AllowInsecureHttp = true, 
              TokenEndpointPath = new PathString("/oauth2/token"),
              AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30), 
              Provider = new SmAuthorizationServerProvider(),
              RefreshTokenProvider = new SmRefreshTokenProvider(),
              AccessTokenFormat = new SmJwtFormat("http://localhost:7814"),
              ApplicationCanDisplayErrors = true,
          };
  
          // Token Generation
          app.UseOAuthAuthorizationServer(OAuthServerOptions);
          app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
      }
  }
  </code></pre>
  
  <p>SmAuthorizationServerProvider.cs</p>
  
  <pre><code>public class SmAuthorizationServerProvider : OAuthAuthorizationServerProvider
  {
      public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
      {
          string clientId = string.Empty;
          string clientSecret = string.Empty;
          Client client = null;
  
          if (!context.TryGetBasicCredentials(out clientId, out clientSecret))
          {
              context.TryGetFormCredentials(out clientId, out clientSecret);
          }
  
          if (context.ClientId == null)
          {
              //Remove the comments from the below line context.SetError, and invalidate context 
              //if you want to force sending clientId/secrects once obtain access tokens. 
              context.Validated();
              //context.SetError("invalid_clientId", "ClientId should be sent.");
              return Task.FromResult&lt;object&gt;(null);
          }
  
          using (AuthRepository _repo = new AuthRepository())
          {
              client = _repo.FindClient(context.ClientId);
          }
  
          if (client == null)
          {
              context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId));
              return Task.FromResult&lt;object&gt;(null);
          }
  
          if (client.ApplicationType == Models.ApplicationTypes.NativeConfidential)
          {
              if (string.IsNullOrWhiteSpace(clientSecret))
              {
                  context.SetError("invalid_clientId", "Client secret should be sent.");
                  return Task.FromResult&lt;object&gt;(null);
              }
              else
              {
                  if (client.Secret != Helper.GetHash(clientSecret))
                  {
                      context.SetError("invalid_clientId", "Client secret is invalid.");
                      return Task.FromResult&lt;object&gt;(null);
                  }
              }
          }
  
          if (!client.Active)
          {
              context.SetError("invalid_clientId", "Client is inactive.");
              return Task.FromResult&lt;object&gt;(null);
          }
  
          context.OwinContext.Set&lt;string&gt;("as:clientAllowedOrigin", client.AllowedOrigin);
          context.OwinContext.Set&lt;string&gt;("as:clientRefreshTokenLifeTime", client.RefreshTokenLifeTime.ToString());
  
          context.Validated();
          return Task.FromResult&lt;object&gt;(null);
      }
  
      public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
      {
          var allowedOrigin = context.OwinContext.Get&lt;string&gt;("as:clientAllowedOrigin");
          SecurityUser user = null;
  
          if (allowedOrigin == null) allowedOrigin = "*";
  
          context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
  
          using (AuthRepository _repo = new AuthRepository())
          {
              user = await _repo.FindUser(context.UserName, context.Password);
  
              if (user == null)
              {
                  context.SetError("invalid_grant", "The user name or password is incorrect.");
                  return;
              }
          }
  
          string scopes = null;
  
          if (context.Scope.Count &gt; 0)
          {
              scopes = string.Join(" ", context.Scope.Select(x =&gt; x.ToString()).ToArray());
          }
  
          var identity = new ClaimsIdentity(context.Options.AuthenticationType);
  
          // Add the user id as claim here 
          // Keep the claims number small, the token length increases with each new claim
          identity.AddClaim(new Claim("sid", user.Id.ToString()));
  
          // add the client id as claim 
          if (!string.IsNullOrEmpty(context.ClientId))
          {
              identity.AddClaim(new Claim("client_id", context.ClientId));
          }
  
          var props = new AuthenticationProperties(new Dictionary&lt;string, string&gt;
          {
              {
                  "as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
              },
              {
                  "as:scope", (scopes == null) ? string.Empty : scopes
              },
              {
                  "userName", context.UserName 
              }
          });
  
  
          var ticket = new AuthenticationTicket(identity, props);
          context.Validated(ticket);
      }
  
      public override Task TokenEndpoint(OAuthTokenEndpointContext context)
      {
          foreach (KeyValuePair&lt;string, string&gt; property in context.Properties.Dictionary)
          {
              context.AdditionalResponseParameters.Add(property.Key, property.Value);
          }
  
          return Task.FromResult&lt;object&gt;(null);
      }
  
      public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
      {
          var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
          var currentClient = context.ClientId;
  
          // check if the token is created with specified client_id
          if (!string.IsNullOrEmpty(currentClient) &amp;&amp; !string.IsNullOrEmpty(originalClient))
          {
              if (originalClient != currentClient)
              {
                  context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
                  return Task.FromResult&lt;object&gt;(null);
              }
          }
  
          var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
  
          // Change auth ticket for refresh token requests if needed
          // newIdentity.AddClaim(new Claim("newClaim", "newValue"));
  
          var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
          context.Validated(newTicket);
  
          return Task.FromResult&lt;object&gt;(null);
      }
  
      public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
      {
          Client client; 
  
          using (AuthRepository _repo = new AuthRepository())
          {
              client = _repo.FindClient(context.ClientId);
          }
  
          var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
          oAuthIdentity.AddClaim(new Claim("client_id", client.Id));
  
          var props = new AuthenticationProperties(new Dictionary&lt;string, string&gt;
          {
              {
                  "as:client_id", (context.ClientId == null) ? string.Empty : context.ClientId
              }
          });
  
          var ticket = new AuthenticationTicket(oAuthIdentity, props);
          context.Validated(ticket);
          return base.GrantClientCredentials(context);
      }
  
  }
  </code></pre>
  
  <p>SmJwtFormat.cs</p>
  
  <pre><code>public class SmJwtFormat : ISecureDataFormat&lt;AuthenticationTicket&gt;
  {
      private const string AudiencePropertyKey = "as:scope";
      private readonly string _issuer = string.Empty;
      private AuthRepository authRepo;
      private SecurityKey signingKey;
  
      private string secret = "P@ssw0rd-7BBF8546-C8C1-44D9-A404-9E1CAF80EC9D-F2FEC38D-2041-499E-9FAA-218C8B1EEC7B";
  
      public SmJwtFormat(string issuer)
      {
          _issuer = issuer;
          authRepo = new AuthRepository();
  
          // Generating the signingKey
          string symmetricKeyAsBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(secret));
  
          var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
  
          signingKey = new SymmetricSecurityKey(keyByteArray);
      }
  
      public string Protect(AuthenticationTicket data)
      {
          if (data == null)
          {
              throw new ArgumentNullException("data");
          }
  
          // The token audience from the JWT terminology is the same as the token Scope in OAuth terminology. 
          string scope = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ? data.Properties.Dictionary[AudiencePropertyKey] : null;
  
          if (string.IsNullOrWhiteSpace(scope)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience/scope");
  
          var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature);
  
          var issued = data.Properties.IssuedUtc;
          var expires = data.Properties.ExpiresUtc;
  
          if (scope != null)
          {
              var scopesList = scope.Split(' ').ToList();
  
              var audClaims = scopesList.Select(s =&gt; new Claim("aud", s));
  
              data.Identity.AddClaims(audClaims);
          }
  
          var token = new JwtSecurityToken(_issuer, null, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingCredentials);
  
  
          var handler = new JwtSecurityTokenHandler();
  
          var jwt = handler.WriteToken(token);
  
          return jwt;
      }
  
      public AuthenticationTicket Unprotect(string protectedText)
      {
          var tokenValidationParameters = new TokenValidationParameters
          {
              ValidIssuer = _issuer,
              IssuerSigningKey = signingKey,
  
          };
  
          var handler = new JwtSecurityTokenHandler();
          SecurityToken token = null;
  
          // Unpack token
          var pt = handler.ReadJwtToken(protectedText);
          string t = pt.RawData;
  
          var principal = handler.ValidateToken(t, tokenValidationParameters, out token);
  
          var identity = principal.Identities;
  
          return new AuthenticationTicket(identity.First(), new AuthenticationProperties());
      }
  }
  </code></pre>
  
  <p>Here is my resource ASP.NET Core 2.2 API Startup.cs</p>
  
  <pre><code>public class Startup
  {
      public Startup(IConfiguration configuration)
      {
          Configuration = configuration;
      }
  
      public IConfiguration Configuration { get; }
  
      public void ConfigureServices(IServiceCollection services)
      {
          services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
          services.AddAuthorization();
  
          // identity http://localhost:7814
          // resource https://localhost:44337
  
          var key = "P@ssw0rd-7BBF8546-C8C1-44D9-A404-9E1CAF80EC9D-F2FEC38D-2041-499E-9FAA-218C8B1EEC7B";
          string symmetricKeyAsBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(key));
          var keyByteArray = Convert.FromBase64String(symmetricKeyAsBase64);
          var securityKey = new SymmetricSecurityKey(keyByteArray);
  
          services.AddAuthentication(options =&gt;
          {
              options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
              options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
  
          }).AddJwtBearer(o =&gt;
          {
              o.Authority = "http://localhost:7814/";
              o.RequireHttpsMetadata = false;
  
              o.TokenValidationParameters = new TokenValidationParameters()
              {
                  ValidateIssuer = true,
                  ValidIssuer = "http://localhost:7814",
                  ValidateAudience = true,
                  ValidAudiences = new List&lt;string&gt;()
                  {
                      "api1" 
                  },
  
                  //IssuerSigningKey = securityKey
              };
          });
      }
  
      public void Configure(IApplicationBuilder app, IHostingEnvironment env)
      {
          if (env.IsDevelopment())
          {
              app.UseDeveloperExceptionPage();
          }
          else
          {
              app.UseHsts();
          }
  
          app.UseHttpsRedirection();
          app.UseAuthentication();
  
          app.UseMvc();
      }
  }
  </code></pre>
  
  <p>Currently I'm able to generate tokens by posting to <a href="http://localhost:7814/oauth2/token" rel="nofollow noreferrer">http://localhost:7814/oauth2/token</a> with the following parameters:</p>
  
  <p>grant_type=password</p>
  
  <p>username=user1</p>
  
  <p>password=p@ssw0rd</p>
  
  <p>client_id=smLocalhost</p>
  
  <p>client_secret=secret</p>
  
  <p>scope=api1 api2</p>
  
  <p>After that the token can be used to access secured endpoints in the resource API. </p>
  
  <p>I've researched <a href="https://github.com/IdentityServer/IdentityServer4" rel="nofollow noreferrer">IdentityServer4</a>, <a href="https://github.com/openiddict/openiddict-core" rel="nofollow noreferrer">OpenIdDict</a> and <a href="https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server" rel="nofollow noreferrer">AspNet.Security.OpenIdConnect.Server</a>, but they seems to be working only with ASP.NET Core. </p>
  
  <p><strong>So after all this stuff, my questions are:</strong></p>
  
  <p><strong>1. How to add OpenID on top of that?</strong></p>
  
  <p><strong>2. Is there a library that I can use?</strong></p>
  
  <p><strong>3. Could you please give me tutorial/advice what can I do to implement
      it?</strong></p>
  
  <p><strong>4. How to implement discovery document endpoint (.well-known/openid-configuration) and rotate the public keys for asymmetric token signing?</strong></p>
  
  <p>Thanks in advance!</p>