I'm playing with Owin.Security.Providers project to allow my MVC 5 app sign in with various social networks. I created an app in LinkedIn and set its scope to r_emailaddress and r_basicprofile. When I try to sign in with LinkedIn accout, I get /Login/SigninRedirect?error=access_denied on my return URL. Though my CallbackPath which is set to /signin-redirect URL has code and state attributes. In Fiddler it looks like this: /signin-redirect?code=<code_id>&state=<state_id>.
Anyone has any ideas what I might be doing wrong?
Here is my code:
protected virtual void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
});
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
var linkedInAuthOptions = new LinkedInAuthenticationOptions()
{
ClientId = <client_id>,
ClientSecret = <client_secret>,
CallbackPath = new PathString("/signin-redirect")
};
app.UseLinkedInAuthentication(linkedInAuthOptions);
}
In my Login controller I handle sign in and the redirect similar to VS2013 example:
public class LoginController : Controller
{
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl)
{
var redirectUrl = "/Login/SigninRedirect";
redirectUrl += string.IsNullOrEmpty(returnUrl) ? "" : "?redirectUrl=" + returnUrl;
return new ChallengeResult(provider, redirectUrl);
}
// Redirects from successful social signin to a speccified URL.
public ActionResult SigninRedirect(string redirectUrl)
{
var redirect = string.IsNullOrEmpty(redirectUrl) ? "/" : redirectUrl;
ExternalLoginInfo loginInfo = AuthenticationManager.GetExternalLoginInfo();
// loginInfo is null when trying to sign in with LinkedIn
// works fine for Twitter though
if (loginInfo != null)
{
var authResult = AuthenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.ExternalCookie).Result;
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
var claims = authResult.Identity.Claims.ToList();
claims.Add(new Claim(ClaimTypes.AuthenticationMethod, loginInfo.Login.LoginProvider));
var claimsIdentity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ExternalCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, claimsIdentity);
}
return new RedirectResult(redirect);
}
#region Helpers
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
private class ChallengeResult : HttpUnauthorizedResult
{
private const string XsrfKey = "XsrfId";
public ChallengeResult(string provider, string redirectUrl, string userId = null)
{
LoginProvider = provider;
RedirectUrl = redirectUrl;
UserId = userId;
}
public string UserId { get; set; }
public string RedirectUrl { get; set; }
public string LoginProvider { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties() {RedirectUri = RedirectUrl};
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
}
}
#endregion
}
The same code works fine when I try to sign in with Twitter account.
Found the issue. The problem was that I registered the same CallbackPath (/signin-redirect) for both Twitter and LinkedIn providers. Like this:
var twitterAuthOptions = new TwitterAuthenticationOptions()
{
ConsumerKey = <consumerKey>,
ConsumerSecret = <consumerSecret>,
CallbackPath = new PathString("/signin-redirect")
};
app.UseTwitterAuthentication(twitterAuthOptions);
var linkedInAuthOptions = new LinkedInAuthenticationOptions()
{
ClientId = <clientId>,
ClientSecret = <clientSecret>,
CallbackPath = new PathString("/signin-redirect")
};
app.UseLinkedInAuthentication(linkedInAuthOptions);
When I set different CallbackPath's, everything started working fine.
Related
There is some problem in my code not sure where I am getting wrong. Earlier same code used to work properly. Now I am trying to log in to application in both the ways i.e through ClaimPrincipal and Claim Identity. In both the ways Sometime data is null. Not sure where is the issue.
Below is my code
StartUp.cs
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = authority,
RedirectUri = redirectUri,
//PostLogoutRedirectUri = redirectUri,
Scope = OpenIdConnectScope.OpenIdProfile,
ResponseType = OpenIdConnectResponseType.IdToken,
TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = false // This is a simplification
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailed
}
}
);
SignIn Method
[AllowAnonymous]
public void SignIn(string ReturnUrl = "/", string loginType = "")
{
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/Account/Office365LoginCallback" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
RedirectURL Code
[AllowAnonymous]
[ExceptionHandler]
public async Task<ActionResult> Office365LoginCallback(string code)
{
var userClaims = User.Identity as System.Security.Claims.ClaimsIdentity;
string userName = userClaims?.FindFirst("name")?.Value;
string userEmail = userClaims?.FindFirst("preferred_username")?.Value;
string userId = userClaims?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
ViewBag.TenantId = userClaims?.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid")?.Value;
return Redirect("~/");
}
catch (Exception ex)
{
throw ex;
}
}
SignOut Method
public ActionResult LogOff()
{
//AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
//HttpContext.GetOwinContext().Authentication.SignOut();
FormsAuthentication.SignOut();
//HttpContext.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
//HttpContext.Session[AppConstants.UserEmail] = null;
//HttpContext.Session[AppConstants.UserUpload] = null;
//HttpContext.Session[AppConstants.UserImage] = null;
//HttpContext.Session[AppConstants.CurrentRole] = null;
//HttpContext.Session[AppConstants.Users] = null;
//Session.Clear();
//Session.Abandon();
return RedirectToAction("Login", "Account");
}
Any Help will be appreciated. FYI I have tried same code in new project there is working fine but here in my old application its not working once clicked on Logout
I receive the following code error when trying to run my MVC application from localhost using Microsoft AD. When debugging the application I noticed that GetExternalLoginInfoAsync returns null. However, the application runs fine on the web. What am I missing? Somebody please help. I have posted my code below the error message.
Server Error in '/' Application.
Object reference not set to an instance of an object.
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.NullReferenceException: Object reference not set to an instance of an object.
Source Error:
Line 140: {
Line 141: ExternalLoginInfo loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
Line 142: ClaimsIdentity claimsIdentity = loginInfo.ExternalIdentity;
Line 143: ApplicationUser applicationUser = new ApplicationUser(claimsIdentity);
Line 144: IdentityResult result = await UserManager.PersistAsync(applicationUser);
My code:
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using SampleQuoteTracker.Models;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace SampleQuoteTracker.Controllers
{
/// <summary>
/// Provides methods for accepting and desplaying account authenication and creating new accounts.
/// </summary>
public class AccountController : Controller
{
//
// GET: /Account/Login
public ActionResult Login(string returnUrl)
{
ViewBag.Title = "Log In";
LoginViewModel loginViewModel = new LoginViewModel()
{
ReturnUrl = returnUrl
};
return View(loginViewModel);
}
//
// POST: /Account/Login
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<JsonResult> Login(LoginViewModel model)
{
// seed return object optimistically
AjaxResultModel loginResult = new AjaxResultModel
{
Status = "Valid",
ReturnUrl = GetLocalUrl(model.ReturnUrl),
Message = null
};
if (Request.IsAjaxRequest())
{
if (!ModelState.IsValid)
{
loginResult.Status = "Invalid";
loginResult.Message = Tools.ListModelStateErrors(ModelState);
}
if (loginResult.Status == "Valid")
{
SignInStatus result = await SignInManager.PasswordSignInAsync(
model.Email,
model.Password,
model.RememberMe,
shouldLockout: false);
switch (result)
{
case SignInStatus.Success:
loginResult.Status = "Success";
break;
case SignInStatus.LockedOut:
loginResult.Status = "LockOut";
loginResult.Message = AlertMessages.AccountLockOut;
break;
case SignInStatus.Failure:
default:
loginResult.Status = "Failure";
loginResult.Message = AlertMessages.AuthenticationFailure;
break;
}
}
}
return Json(loginResult);
}
//
// POST: /Account/LogOff
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
//AuthenticationManager.SignOut();
//return RedirectToAction("SignOutCallback");
return null;
}
public void SignIn(string returnUrl = "/")
{
if (returnUrl == "/")
{
returnUrl = Request.ApplicationPath;
}
Uri baseUri = new UriBuilder(Request.Url.Scheme, Request.Url.Host, Request.Url.Port).Uri;
Uri uri = new Uri(baseUri, returnUrl);
// If this action is called and the user is already authenticated,
// it means the user is not a member of the appropriate role for
// the controller/action requested.
if (Request.IsAuthenticated)
{
RouteValueDictionary values = RouteDataContext.RouteValuesFromUri(uri);
string controllerName = (string)values["controller"];
string actionName = (string)values["action"];
string errorUrl = Url.Action("Error",
routeValues: new
{
message = "You are not authorized to view this content",
controllerName,
actionName
});
Response.Redirect(errorUrl, true);
}
else
{
// https://stackoverflow.com/a/21234614
// Activate the session before login to generate the authentication cookie correctly.
Session["Workaround"] = 0;
// Send an OpenID Connect sign-in request.
string externalLoginCallback = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl });
AuthenticationManager.Challenge(new AuthenticationProperties { RedirectUri = externalLoginCallback },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
//public IAuthenticationManager AuthenticationManager
//{
// get { return HttpContext.GetOwinContext().Authentication; }
//}
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
ExternalLoginInfo loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
ClaimsIdentity claimsIdentity = loginInfo.ExternalIdentity;
ApplicationUser applicationUser = new ApplicationUser(claimsIdentity);
IdentityResult result = await UserManager.PersistAsync(applicationUser);
if (result.Succeeded)
{
claimsIdentity = await applicationUser.GenerateUserIdentityAsync(UserManager);
}
AuthenticationManager.SignIn(new AuthenticationProperties { IsPersistent = false }, claimsIdentity);
return Redirect(returnUrl);
}
[Authorize]
public void SignOut()
{
string callbackUrl = Url.Action("SignOutCallback", "Account", null, Request.Url.Scheme);
AuthenticationProperties properties = new AuthenticationProperties { RedirectUri = callbackUrl };
AuthenticationManager.SignOut(
properties,
OpenIdConnectAuthenticationDefaults.AuthenticationType,
CookieAuthenticationDefaults.AuthenticationType,
Microsoft.AspNet.Identity.DefaultAuthenticationTypes.ApplicationCookie);
}
public ActionResult Error(string message, string controllerName = "Account", string actionName = "SignIn")
{
Exception exception = new Exception(message);
HandleErrorInfo handleErrorInfo = new HandleErrorInfo(exception, controllerName, actionName);
return View("Error", handleErrorInfo);
}
public ActionResult SignOutCallback()
{
if (Request.IsAuthenticated)
{
// Redirect to home page if the user is authenticated.
return RedirectToAction("Index", "Home");
}
return View();
}
protected override void Dispose(bool disposing)
{
if (disposing && UserManager != null)
{
UserManager.Dispose();
UserManager = null;
}
base.Dispose(disposing);
}
#region Helpers
private ApplicationSignInManager _signInManager;
private ApplicationUserManager _userManager;
private IAuthenticationManager _authenticationManager;
// Used for XSRF protection when adding external logins
private const string XsrfKey = "XsrfId";
/// <summary>
/// Gets a reference to the <see cref="ApplicationSignInManager"/>.
/// </summary>
protected ApplicationSignInManager SignInManager
{
get
{
return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
}
private set { _signInManager = value; }
}
protected ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
protected IAuthenticationManager AuthenticationManager
{
get
{
return _authenticationManager ?? HttpContext.GetOwinContext().Authentication;
}
private set
{
_authenticationManager = value;
}
}
/// <summary>
/// Ensures the <paramref name="returnUrl"/> belongs to this application.
/// <para>We don't want to redirect to a foreign page after authentication.</para>
/// </summary>
/// <param name="returnUrl">a <see cref="System.String"/> containing the page address that required authorization.</param>
/// <returns>a <see cref="System.String"/> containing a local page address.</returns>
private string GetLocalUrl(string returnUrl)
{
if (!Url.IsLocalUrl(returnUrl))
{
return Url.Action("Index", "Home");
}
return returnUrl;
}
private class ChallengeResult : HttpUnauthorizedResult
{
public ChallengeResult(string provider, string redirectUri)
: this(provider, redirectUri, null)
{
}
public ChallengeResult(string provider, string redirectUri, string userId)
{
LoginProvider = provider;
RedirectUri = redirectUri;
UserId = userId;
}
public string LoginProvider { get; set; }
public string RedirectUri { get; set; }
public string UserId { get; set; }
public override void ExecuteResult(ControllerContext context)
{
var properties = new AuthenticationProperties() { RedirectUri = RedirectUri };
if (UserId != null)
{
properties.Dictionary[XsrfKey] = UserId;
}
context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
}
}
private class RouteDataContext : HttpContextBase
{
public override HttpRequestBase Request { get; }
private RouteDataContext(Uri uri)
{
string url = uri.GetLeftPart(UriPartial.Path);
string qs = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped);
Request = new HttpRequestWrapper(new HttpRequest(null, url, qs));
}
public static RouteValueDictionary RouteValuesFromUri(Uri uri)
{
return RouteTable.Routes.GetRouteData(new RouteDataContext(uri)).Values;
}
}
#endregion
}
}
Eventually the ExternalCookie is removed when the Owin middleware inspects the context.
That way AuthenticationManager.GetExternalLoginInfo() returns null after logging in, the cookie holding the info has been removed and replaced by a ApplicationCookie.
So add the following in your Startup.cs
public void ConfigureAuth(IAppBuilder app)
{
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/LogOn")
});
....
}
For more details, you could refer to this article.
I wanted to make JWT auth in my MVC app. I make Authorization web service in Web API which returns token correctly. After that Im trying to store token in cookie.
[HttpPost]
public async Task<ActionResult> Login(LoginDto loginDto)
{
var token = await loginService.GetToken(loginDto);
if (!string.IsNullOrEmpty(token))
{
var cookie = new System.Web.HttpCookie("token", token)
{
HttpOnly = true
};
Response.Cookies.Add(cookie);
return RedirectToAction("Index", "Product");
}
return View("LoginFailed");
}
But now I wanted to add this token to headers for every request. So I decided action filters would be best for achieve this.
public class CustomActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var token = filterContext.HttpContext.Request.Cookies.Get("token");
if (token != null)
filterContext.HttpContext.Request.Headers.Add("Authorization", $"Bearer {token}");
base.OnActionExecuting(filterContext);
}
}
Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
AutofacConfig.Configure();
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
ConfigureOAuth(app);
}
public void ConfigureOAuth(IAppBuilder app)
{
var issuer = System.Configuration.ConfigurationManager.AppSettings["issuer"];
var audience = System.Configuration.ConfigurationManager.AppSettings["appId"];
var secret = TextEncodings.Base64Url.Decode(System.Configuration.ConfigurationManager.AppSettings["secret"]);
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audience },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, secret)
},
});
}
}
And then I just marked controller which authorize attribute. It works fine when I called it with POSTMAN.
But action filters in MVC are fired always after authorization filter. So I have questions:
How to add token from cookie to every request ? Is it good practice? If not what I should do?
How about csrf attacks and others ? Is AntiForgeryTokenAttr will do the work? What about ajax calls then?
Additional Information
This is how login service looks like. Its just making call to auth endpoint.
public class LoginService : ILoginService
{
public async Task<string> GetToken(LoginDto loginDto)
{
var tokenIssuer = ConfigurationManager.AppSettings["issuer"];
using (var httpClient = new HttpClient {BaseAddress = new Uri($"{tokenIssuer}/oauth2/token")})
{
using (var response = await httpClient.PostAsync(httpClient.BaseAddress, new FormUrlEncodedContent(
new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>("username", loginDto.Username),
new KeyValuePair<string, string>("password", loginDto.Password),
new KeyValuePair<string, string>("grant_type", "password"),
new KeyValuePair<string, string>("client_id", ConfigurationManager.AppSettings["appId"])
})))
{
var contents = await response.Content.ReadAsStringAsync();
if (response.StatusCode == HttpStatusCode.OK)
{
var deserializedResponse =
new JavaScriptSerializer().Deserialize<Dictionary<string, string>>(contents);
var token = deserializedResponse["access_token"];
return token;
}
}
return null;
}
}
}
I found a solution. I just make custom OAuthBearerAuthenticationProvider provider and inside this class Im retrieve token from cookie and then assign this to context.Token
public class MvcJwtAuthProvider : OAuthBearerAuthenticationProvider
{
public override Task RequestToken(OAuthRequestTokenContext context)
{
var token = context.Request.Cookies.SingleOrDefault(x => x.Key == "token").Value;
context.Token = token;
return base.RequestToken(context);
}
}
And then inside startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
AutofacConfig.Configure();
AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
ConfigureOAuth(app);
}
public void ConfigureOAuth(IAppBuilder app)
{
var issuer = System.Configuration.ConfigurationManager.AppSettings["issuer"];
var audience = System.Configuration.ConfigurationManager.AppSettings["appId"];
var secret = TextEncodings.Base64Url.Decode(System.Configuration.ConfigurationManager.AppSettings["secret"]);
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audience },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, secret)
},
Provider = new MvcJwtAuthProvider() // override custom auth
});
}
}
For my WebAPI, I'm using:
public void ConfigureAuth( IAppBuilder app )
{
app.UseIdentityServerBearerTokenAuthentication( new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings[ "ida:Authority" ],
RequiredScopes = new[ ]
{
"XXXXAPI"
}
} );
}
I authenticate okay, but I need to get my roles and other information supplied by the UserInfo endpoint.
Does UseIdentityServerBearerTokenAuthentication do this automatically or is there an event like OpenIdConnectAuthenticationNotifications.AuthroizationCodeReceived that I should be using to set the ClaimsIdentity?
app.UseIdentityServerBearerTokenAuthentication does set the roles and scopes automatically.
For some unkown reason, I only returned data from the GetProfileDataAsync of the UserService only when the caller was UserInfoEndpoint. Once I got rid of this piece of code, all the roles were automatically populated.
You must use OAuth2+JWT and some custom configuration to save user roles and other claims in access token.
Add these values in Web.config
<appSettings>
<add key="as:AudienceId" value="414e1927a3884f68abc79f7283837fd1" />
<add key="as:AudienceSecret" value="qMCdFDQuF23RV1Y-1Gq9L3cF3VmuFwVbam4fMTdAfpo" />
</appSettings>
Write a CustomJwtFormat class
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.DataHandler.Encoder;
using System;
using System.Configuration;
using System.IdentityModel.Tokens;
using Thinktecture.IdentityModel.Tokens;
public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly string _issuer = string.Empty;
public CustomJwtFormat(string issuer)
{
_issuer = issuer;
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
string symmetricKeyAsBase64 = ConfigurationManager.AppSettings["as:AudienceSecret"];
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingKey = new HmacSigningCredentials(keyByteArray);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.WriteToken(token);
return jwt;
}
}
Create a custom authenticationProvider
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult<object>(null);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
using (AuthRepository _repo = new AuthRepository())
{
User user = await _repo.FindUser(context.UserName, context.Password);
if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
}
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("unique_name", context.UserName));
identity.AddClaim(new Claim("role", "user"));
context.Validated(identity);
}
}
Configure Your Custom Setting
private static void ConfigureOAuthTokenGeneration(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new ApplicationOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat("http://localhost/")
};
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
private static void ConfigureOAuthTokenConsumption(IAppBuilder app)
{
var issuer = "http://localhost/";
string audienceId = ConfigurationManager.AppSettings["as:AudienceId"];
byte[] audienceSecret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["as:AudienceSecret"]);
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audienceId },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
}
});
}
public void Configuration(IAppBuilder app(
{
app.UseAutofacMvc();
ConfigureOAuthTokenGeneration(app);
ConfigureOAuthTokenConsumption(app);
//other codes
}
If you want to get userInfo from access token, do it as follows:
public static string GetUserNameFromOAuth(HttpRequestMessage Request)
{
if (Request.Headers.Contains("Authorization"))
{
var authHeader = Request.Headers.GetValues("Authorization");
var authEncoded = authHeader.FirstOrDefault();
var authList = authEncoded.Split(' ');
var payload = authList[1];
var symmetricKeyAsBase64 = ConfigurationManager.AppSettings["as:AudienceSecret"];
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingKey = new HmacSigningCredentials(keyByteArray);
string token = JWT.JsonWebToken.Decode(payload, keyByteArray);
var jsonObject = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(token);
var userName = jsonObject.FirstOrDefault(p => p.Key == "unique_name").Value;
return userName;
}
return "";
}
Test in postman:
I have followed the tutorials up till this point in the series. I am using one project in the solution that acts as both the token issuing authority as well as the resource server.
The JWT is generated using the endpoint mentioned in the startup class and I validated it on jwt.io as well. However when I pass this JWT using Postman on Chrome to the resource API end point secured with an Authorize attribute, I always find it returning
{
"message": "Authorization has been denied for this request." }
The other api method as in the api controller class below works when called thru Postman on Chrome.
I have used the latest versions of all dlls required from the nuget console
Code in the startup class
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuthTokenGeneration(app);
ConfigureOAuthTokenConsumption(app);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
}
private void ConfigureOAuthTokenGeneration(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
//For Dev enviroment only (on production should be AllowInsecureHttp = false)
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/oauth/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new CustomOAuthProvider(),
AccessTokenFormat = new CustomJwtFormat(ConfigurationManager.AppSettings["Issuer"]),
};
// OAuth 2.0 Bearer Access Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
}
private void ConfigureOAuthTokenConsumption(IAppBuilder app)
{
string issuer = ConfigurationManager.AppSettings["Issuer"];
string audienceId = ConfigurationManager.AppSettings["AudienceId"];
byte[] audienceSecret = TextEncodings.Base64Url.Decode(ConfigurationManager.AppSettings["AudienceSecret"]);
// Api controllers with an [Authorize] attribute will be validated with JWT
app.UseJwtBearerAuthentication(
new JwtBearerAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AllowedAudiences = new[] { audienceId },
IssuerSecurityTokenProviders = new IIssuerSecurityTokenProvider[]
{
new SymmetricKeyIssuerSecurityTokenProvider(issuer, audienceSecret)
}
});
}
Code in the Custom OAuthProvider
public class CustomOAuthProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult<object>(null);
}
public override Task MatchEndpoint(OAuthMatchEndpointContext context)
{
//avoid pre-flight calls
if (context.OwinContext.Request.Method == "OPTIONS" && context.IsTokenEndpoint)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Methods", new[] { "POST" });
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "accept", "authorization", "content-type" });
context.OwinContext.Response.StatusCode = 200;
context.RequestCompleted();
return Task.FromResult<object>(null);
}
return base.MatchEndpoint(context);
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
//setting up claims in the constructor of class UserDetails
UserDetails user = new UserDetails();
user.UserName = context.UserName;
user.FirstName = "Dummy First";
user.LastName = "Dummy Last";
ClaimsIdentity identity = new ClaimsIdentity("JWT-BearerAuth-Test");
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
foreach (string claim in user.Claims)
{
identity.AddClaim(new Claim(ClaimTypes.Role, claim));
}
var ticket = new AuthenticationTicket(identity, null);
context.Validated(ticket);
}
}
The custom JWT class
public class CustomJwtFormat : ISecureDataFormat<AuthenticationTicket>
{
private readonly string _issuer = string.Empty;
public CustomJwtFormat(string issuer)
{
_issuer = issuer;
}
public string Protect(AuthenticationTicket data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
string audienceId = ConfigurationManager.AppSettings["AudienceId"];
string symmetricKeyAsBase64 = ConfigurationManager.AppSettings["AudienceSecret"];
var keyByteArray = TextEncodings.Base64Url.Decode(symmetricKeyAsBase64);
var signingKey = new HmacSigningCredentials(keyByteArray);
var issued = data.Properties.IssuedUtc;
var expires = data.Properties.ExpiresUtc;
var token = new JwtSecurityToken(_issuer, audienceId, data.Identity.Claims, issued.Value.UtcDateTime, expires.Value.UtcDateTime, signingKey);
var handler = new JwtSecurityTokenHandler();
var jwt = handler.WriteToken(token);
return jwt;
}
}
The Resource server's Api controller
public class AdminController : ApiController
{
//This call works
public IHttpActionResult ReadData(string id)
{
return Ok("ID sent in:" + id);
}
//[Authorize(Roles="EditRecord")] //doesnt work
[Authorize] //doesnt work either
public IHttpActionResult EditData(string id)
{
return Ok("Edited ID:" + id);
}
}
My environment is VS2013 with Framework 4.5 using OAuth2 with Web Api 2. Please excuse the long post.
You need to make sure that values for issuer, audienceId, and audienceSecret used in method "ConfigureOAuthTokenConsumption" are the same values used when you generated the JWT token, take care of trailing slashes "/".
This is the only thing comes to my mind right now.