I have a Mobile App I am writing, at present it is simply the To Do Item list quick start application with custom Authentication added. I have the associated Xamarin Forms app.
From the App I am able to login using the LoginAsync method, my website returns a token and shows the username I am logging in as, but subsequent calls suggest I am not authorised.
After a bit of debugging, I can see that the request arrives at the web server with the X-ZUMO-AUTH header and the token in the value, but I can see that the User does not seem to be populated and the call to the GetAllTodoItems method is returned as 401:Unauthorized.
In the startup code for the website, the ConfigureMobileApp contains the following:
app.UseWebApi(config);
if (string.IsNullOrEmpty(settings.HostName))
{
// This middleware is intended to be used locally for debugging. By default, HostName will
// only have a value when running in an App Service application.
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
SigningKey = ConfigurationManager.AppSettings["SigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["ValidAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["ValidIssuer"] },
TokenHandler = config.GetAppServiceTokenHandler()
});
}
I have an account controller class:
[Route(".auth/login/custom")]
public class AccountController : ApiController
{
private static string URL = "https://myapidev.azurewebsites.net/";
private static string KEY = "FC31EB8CAAAAAA9D74EEE3613A7A08CA65CB1ACAA8CEFF82A5B5E915625B31D";
public AccountController()
{
}
[HttpPost]
public IHttpActionResult Post([FromBody] LoginUser assertion)
{
if (isValidAssertion(assertion))
{
JwtSecurityToken token = AppServiceLoginHandler.CreateToken(new Claim[] { new Claim(JwtRegisteredClaimNames.Sub, assertion.username) },
ConfigurationManager.AppSettings["SigningKey"],
ConfigurationManager.AppSettings["ValidAudience"],
ConfigurationManager.AppSettings["ValidIssuer"],
TimeSpan.FromHours(24));
return Ok(new LoginResult()
{
authenticationToken = token.RawData,
user = new LoginResultUser() { userId = assertion.username }
});
}
else // user assertion was not valid
{
return ResponseMessage(Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Invalid Request"));
}
}
private bool isValidAssertion(LoginUser assertion)
{
return assertion != null;
}
}
The TodoItemController contains the following:
[Authorize]
[MobileAppController]
public class TodoItemController : TableController<TodoItem>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
EducaterAPIDevContext context = new EducaterAPIDevContext();
DomainManager = new EntityDomainManager<TodoItem>(context, Request);
//// Get the SID of the current user.
//var claimsPrincipal = this.User as ClaimsPrincipal;
//string sid = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier).Value;
}
// GET tables/TodoItem
public IQueryable<TodoItem> GetAllTodoItems()
{
return Query();
}
...
...
}
On calling the query method from the Xamarin App, it returns with 401 even though the X-ZUMO-AUTH is in the headers and contains the correct token issued by the login method.
Have I missed something or has anyone come across this issue before - any help would be appreciated?
Have you turned on Authentication/Authorization in your App Service? Without it, the token will never be decoded.
This is the most common issue.
After digging I found the issue, initially there was a configuration issue - the above comments helped thanks. The Audiences and Issuers must match your azure site including including the trailing slash.
The issue once the configuration had been corrected was that the token which is passed correctly from my App did not get processed at the server side so all Authorized areas where out-of-bounds. This was because of the order of calls in the ConfigureMobileApp method. I was calling the app.UseWebApi method before the app.UseAppServiceAuthentication method, changing the order suddenly had the token being tested again.
The dummy site I have working now has the following:
public static void ConfigureMobileApp(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
//For more information on Web API tracing, see http://go.microsoft.com/fwlink/?LinkId=620686
SystemDiagnosticsTraceWriter traceWriter = config.EnableSystemDiagnosticsTracing();
new MobileAppConfiguration()
.UseDefaultConfiguration()
.MapApiControllers()
.ApplyTo(config);
config.MapHttpAttributeRoutes();
// Use Entity Framework Code First to create database tables based on your DbContext
//Database.SetInitializer(new EducaterAPIDevInitializer());
// To prevent Entity Framework from modifying your database schema, use a null database initializer
// Database.SetInitializer<EducaterAPIDevContext>(null);
MobileAppSettingsDictionary settings = config.GetMobileAppSettingsProvider().GetMobileAppSettings();
if (string.IsNullOrEmpty(settings.HostName))
{
var options = new AppServiceAuthenticationOptions
{
SigningKey = ConfigurationManager.AppSettings["SigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["ValidAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["ValidIssuer"] },
TokenHandler = config.GetAppServiceTokenHandler()
};
app.UseAppServiceAuthentication(options);
}
app.UseWebApi(config);
}
Related
I am trying to cache Access Token using MSAL by following the tutorial provided here: https://github.com/Azure-Samples/ms-identity-aspnet-webapp-openidconnect
I am using ASP.NET MVC on .NET 4.7.2.
But I am getting error when calling an Microsoft Graph API by getting the token from cache.
I'm getting the error when my code hits this line:
result = app.AcquireTokenSilent(scopes, account).ExecuteAsync().Result;
Following the steps when I get the issue.
Run the code from Visual Studio.
Code hit OnAuthorizationCodeReceived()
Able to get the data from Microsoft.Graph
Sign-in is successfully.
Close the browser.
Sign back in.
Code doesn't hit OnAuthorizationCodeReceived().
Call the Microsoft.Graph
Error, IAccount is null (no token found in cache). I expected to get the token from cache
Sign-in again.
Code hit the OnAuthorizationCodeReceived().
The code I am using:
Startup.cs:
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification context)
{
IConfidentialClientApplication clientApp = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = await clientApp.AcquireTokenByAuthorizationCode(new[] { "User.Read" }, context.Code)
.ExecuteAsync();
}
Class to store token in cache
public static class MsalAppBuilder
{
public static string GetAccountId(this ClaimsPrincipal claimsPrincipal)
{
string oid = claimsPrincipal.GetObjectId();
string tid = claimsPrincipal.GetTenantId();
return $"{oid}.{tid}";
}
private static IConfidentialClientApplication clientapp;
public static IConfidentialClientApplication BuildConfidentialClientApplication()
{
if (clientapp == null)
{
clientapp = ConfidentialClientApplicationBuilder.Create(Globals.clientId)
.WithClientSecret(Globals.clientSecret)
.WithRedirectUri(Globals.redirectUri)
.WithAuthority(new Uri(Globals.authority))
.Build();
// In-memory distributed token cache
clientapp.AddDistributedTokenCache(services =>
{
services.AddDistributedMemoryCache();
services.Configure<MsalDistributedTokenCacheAdapterOptions>(o =>
{
o.Encrypt = true;
});
});
}
return clientapp;
}
}
public static string GetData()
{
IConfidentialClientApplication app = MsalAppBuilder.BuildConfidentialClientApplication();
AuthenticationResult result = null;
var account = app.GetAccountAsync(ClaimsPrincipal.Current.GetAccountId()).Result;
string[] scopes = { "User.Read" };
try
{
// try to get an already cached token
result = app.AcquireTokenSilent(scopes, account).ExecuteAsync().Result;// ConfigureAwait(false);
//some functionality here
}
catch (Exception ex)//MsalUiRequiredException
{
return "error";
}
}
You should clear the token cache because there may have been a cache not Encrypt before
Or you adjust this code
services.Configure<MsalDistributedTokenCacheAdapterOptions>(o =>
{
o.Encrypt = false;
});
In my AccountController I have the following methods:
/*
* Called when requesting to sign up or sign in
*/
public void SignUpSignIn(string redirectUrl)
{
redirectUrl = redirectUrl ?? "/";
// Use the default policy to process the sign up / sign in flow
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = redirectUrl });
return;
}
/*
* Called when requesting to sign up
*/
public void SignUp()
{
// Use the default policy to process the sign up flow
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = "/" }, Globals.SignUpPolicyId);
return;
}
The UserFlow is set up inside of Azure, called B2C_1_signup, and that's what Globals.SignUpPolicyId evaluates to. Yet, whenever I test it out, I get an HTTP 401 error.
Here's the razor code that creates my button/link:
#Html.ActionLink("Sign Up!", "SignUp", "Account", routeValues: null, htmlAttributes: new { id = "signUpLink", #class = "btn btn-default" })
Whenever I test the link provided by Microsoft inside of the B2C Tenant, it brings up the Sign Up page correctly.
Here's the cleansed link provided by Microsoft for testing:
https://mytenantname.b2clogin.com/mytenantname.onmicrosoft.com/oauth2/v2.0/authorize?p=B2C_1_signup&client_id=RANDOM_GUID&nonce=defaultNonce&redirect_uri=http%3A%2F%2Flocalhost%3A1111&scope=openid&response_type=id_token&prompt=login
What am I missing??
• The redirect URI string defined in the account controller should be defined in the app config settings as a private static string and the B2C policies as different identifiers as public static strings due to which when during the user flow, authentication redirection will happen through by referencing the concerned app config string rather than finding it in the controller file itself. Since, you are encountering HTTP 401 error due to authentication issues related to the browser session.
Please find below the app controller sample methods calling the Azure AD B2C policies which works correctly as defined below for sign up, sign in and profile of
the user to be authenticated: -
public class AccountController : Controller
{
public void SignIn()
{
if (!Request.IsAuthenticated)
{
// To execute a policy, you simply need to trigger an OWIN challenge.
// You can indicate which policy to use by specifying the policy id as the AuthenticationType
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" }, Startup.SignInPolicyId);
}
}
public void SignUp()
{
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" }, Startup.SignUpPolicyId);
}
}
public void Profile()
{
if (Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" }, Startup.ProfilePolicyId);
}
}
public void SignOut()
{
// To sign out the user, you should issue an OpenIDConnect sign out request
if (Request.IsAuthenticated)
{
IEnumerable<AuthenticationDescription> authTypes = HttpContext.GetOwinContext().Authentication.GetAuthenticationTypes();
HttpContext.GetOwinContext().Authentication.SignOut(authTypes.Select(t => t.AuthenticationType).ToArray());
}
}
}
Also, refer the below link for more clarified information: -
https://bitoftech.net/2016/08/31/integrate-azure-ad-b2c-asp-net-mvc-web-app/
Also, find the below gif output for reference: -
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
I Have created a MVC Controller, Please find the below code
Now i got a requirement to create with OAuth
I have created Client Id, Client Secret, Resource and Secrect Key in Azure,
and using that i can able to get Bearer Token
Now Could any one help me to Authenticate in My MVC controller and redirect to bankCreationController using c#
public class BankCreationController : ApiController
{
[HttpPost]
[Route("~/BankCreationController ")]
public string BankCreationController (sting s)
{
}
}
I got a chance to setup a Bearer Authentication with .Net MVC 5, and here is how I set up it using a custom attribute. I think we need to customize a little to make it work for .Net Core
The api needs to have an annotation, in this case I named it BearerAuthentication, so that it will check the header of the request for the bearer authentication before it can reach inside the method of BankCreationMethod
[HttpPost]
[BearerAuthentication]
[Route("someurl")]
public string BankCreationMethod(string s){
}
Then create a custom attribute class name BearerAuthentication, inherits from ActionFilterAttribute. Every request to the actions that have annotation [BearerAuthentication] will be checked by this method OnActionExecuting. If the header of the request is Bearer type and has a valid token, this will return a valid result, otherwise will be a HttpStatusCode.Unauthorized response to the action.
public class BearerAuthenticationAttribute : System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext context)
{
// 1. Look for credentials in the request.
HttpRequestMessage request = context.Request;
AuthenticationHeaderValue authorization = request.Headers.Authorization;
// 2. If there are no credentials, do nothing.
if (authorization == null)
{
context.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
return;
}
// 3. If there are credentials but the filter does not recognize the
// authentication scheme, do nothing.
if (authorization.Scheme != "Bearer")
{
context.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
return;
}
if (String.IsNullOrEmpty(authorization.Parameter))
{
context.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
return;
}
//4.If there are credentials that the filter understands, try to validate them.
if (!String.IsNullOrEmpty(authorization.Parameter))
{
var apiKey = string.Empty;
if (!TokenService.IsValidToken(authorization.Parameter))
{
context.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
return;
}
return;
}
}
}
The service to check if the token from the request is valid and the algorithm to encrypt the token could be something this way with methods GenerateToken() and IsValidToken(string). The algorithm in this example is not like the Bearer token you generate from Azure but you can customize it somehow to match your case:
public static class TokenService
{
public static int expireTime = 20;
public static string GenerateToken()
{
byte[] time = BitConverter.GetBytes(DateTime.UtcNow.ToBinary());
var key = Guid.NewGuid().ToString();
var salt = "somesalt";
byte[] securedKey = Encoding.ASCII.GetBytes(key + salt);
string token = Convert.ToBase64String(time.Concat(securedKey).ToArray());
return token;
}
public static bool IsValidToken(string token)
{
try
{
byte[] data = Convert.FromBase64String(token);
//default expire time is 20 mins
DateTime when = DateTime.FromBinary(BitConverter.ToInt64(data, 0));
if (when < DateTime.UtcNow.AddMinutes(-expireTime))
{
return false;
}
return true;
}
catch (Exception ex)
{
new Exception("Unauthorized!");
}
return false;
}
}
Then in the Startup.cs file, setup the AuthenticationTokenProvider (in this example is Microsoft.Owin.Security.Infrastructure) so that the app understand the method GenerateToken() from the TokenService above
public class Startup
{
public void ConfigureOAuth(IAppBuilder app)
{
AuthenticationTokenProvider authTokenProvider = new AuthenticationTokenProvider();
authTokenProvider.OnCreate = (context) =>
{
context.SetToken(TokenService.GenerateToken());
};
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
Provider = new SimpleAuthorizationServerProvider(),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(TokenService.expireTime),
AccessTokenProvider = authTokenProvider,
AuthorizationCodeProvider = authTokenProvider
};
// Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
}
}
Hope this helps!
I am retrieving the sid in my WebApi controller using
private string GetAzureSID()
{
var principal = this.User as ClaimsPrincipal;
var nameIdentifier = principal.FindFirst(ClaimTypes.NameIdentifier);
if (nameIdentifier != null)
{
var sid = nameIdentifier.Value;
return sid;
}
return null;
}
And I get a non-null value. However, when I try to call specific hub clients using
hubContext.Clients.User(sid).refresh()
the expected clients do not respond. Actually no clients respond. That said
hubContext.Clients.All.refresh()
does call everyone. I have not done anything like
var idProvider = new PrincipalUserIdProvider();
GlobalHost.DependencyResolver.Register (typeof(IUserIdProvider), () => idProvider);
But I think that should be the default right? What am I missing? Perhaps there is some way of checking what userIds are in Clients?
Update. I found this Context.User.Identity.Name is null with SignalR 2.X.X. How to fix it? which talks about having signalr before webapi, which I tried to no avail. I am using authentication from Azure though, so that could be the issue. HEre is what my ConfigureMobileApp looks like
public static void ConfigureMobileApp(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.UseDefaultConfiguration()
.ApplyTo(config);
// Use Entity Framework Code First to create database tables based on your DbContext
// Database.SetInitializer(new MobileServiceInitializer());
var migrator = new DbMigrator(new Migrations.Configuration());
migrator.Update();
MobileAppSettingsDictionary settings = config.GetMobileAppSettingsProvider().GetMobileAppSettings();
if (string.IsNullOrEmpty(settings.HostName))
{
app.UseAppServiceAuthentication(new AppServiceAuthenticationOptions
{
// This middleware is intended to be used locally for debugging. By default, HostName will
// only have a value when running in an App Service application.
SigningKey = ConfigurationManager.AppSettings["SigningKey"],
ValidAudiences = new[] { ConfigurationManager.AppSettings["ValidAudience"] },
ValidIssuers = new[] { ConfigurationManager.AppSettings["ValidIssuer"] },
TokenHandler = config.GetAppServiceTokenHandler()
});
}
app.MapSignalR();
app.UseWebApi(config);
}
It could be that the problem is that the authentication is somehow coming after from Azure? I tried calling the Hub from my Client
[Authorize]
public class AppHub : Hub
{
public string Identify()
{
return Context.User.Identity.Name;
}
}
but the result is 'null' so I think that Signalr is unable to get the User correctly.
Update 2. Could be I need to create a UseOAuthBearerAuthentication that reads [x-zumo-auth]
Update 3. I added some more functions into my Hub
public string Identify()
{
return HttpContext.Current.User.Identity.Name;
}
public bool Authenticated()
{
return Context.User.Identity.IsAuthenticated;
}
public string Bearer()
{
return Context.Headers["x-zumo-auth"];
}
and the results are
null
true
the correct bearer token
Not sure if this helps, but the sid from WebApi look like sid:8ba1a8532eaa6eda6758c3e522f77c24
Update 4. I found the sid! I changed my hub code to
public string Identify()
{
// return HttpContext.Current.User.Identity.Name;
var identity = (ClaimsIdentity)Context.User.Identity;
var tmp = identity.FindFirst(ClaimTypes.NameIdentifier);
return tmp.Value;
}
and I got the sid. Not sure how Context.User.Identity.Name is different than this, but this does work. Now the question is, how can I use a given sid to call
hubContext.Clients.User(...???...).refresh()
if I know the NameIdentifier of the user?
Special thanks to #davidfowler for the remarkably annoying and yet astute "why would it not be null :smile:". Once I finally accepted that Context.User.Identity.Name would always be null, I was able to get the hub to retrieve the sid using
var identity = (ClaimsIdentity)Context.User.Identity;
var tmp = identity.FindFirst(ClaimTypes.NameIdentifier);
return tmp.Value;
which led me to look through the signalr code for User.Identity.Name ultimately landing on PrincipalUserIdProvider. Surprise, surprise, it assigns GetUserId based on User.Identity.Name. I created a new IUserIdProvider:
public class ZumoUserIdProvider : IUserIdProvider
{
public string GetUserId(IRequest request)
{
if (request == null)
{
throw new ArgumentNullException("request");
}
if (request.User != null && request.User.Identity != null)
{
var identity = (ClaimsIdentity)request.User.Identity;
var identifier = identity.FindFirst(ClaimTypes.NameIdentifier);
if (identifier != null)
{
return identifier.Value;
}
}
return null;
}
}
and registered it before anything else in Startup.cs
public void Configuration(IAppBuilder app)
{
var userIdProvider = new ZumoUserIdProvider();
GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => userIdProvider);
ConfigureMobileApp(app);
}
and like magic, I can now hubContext.Clients.User(sid).refresh(). Hope this helps someone out there.
I'm working on an MVC 6 application that does not use Entity or Identity. Instead, I'm using Dapper. I have a controller that accepts a POST request and uses Dapper to check the database to see if the users name / password match. All I'd like to do is store the users name and whether they're logged in or not so I can make that check on subsequent pages.
I looked around and it seems like using Cookie based authentication should allow me to do what I want. Here's the relevant code in my Startup.cs file:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
LoginPath = "/account/login",
AuthenticationScheme = "Cookies",
AutomaticAuthenticate = true,
AutomaticChallenge = true
});
Here's what the relevant code in my controllers login action looks like:
var user = _repo.FindByLogin(model.VendorId, model.Password);
if (user != null) {
var claims = new List < Claim > {
new Claim("VendorId", user.VendorId),
new Claim("Name", "john")
};
var id = new ClaimsIdentity(claims, "local", "name", "role");
await HttpContext.Authentication.SignInAsync("Cookies", new ClaimsPrincipal(id));
var l = ClaimsIdentity.DefaultNameClaimType;
return RedirectToAction("Index", "PurchaseOrders");
}
The above code seems to work in that a cookie is being saved, but I'm not sure how I would go about getting the user information back out of the cookie (or even how to retrieve the cookie on subsequent requests in the first place).
In my mind I'm imagining being able to do something like: var user = (User)HttpContext.Request.Cookies.Get(????), but I'm not sure if that's practical or not.
You can get the user data back by using the ClaimsPrincipal.FindFirstValue(xxx)
here is my example class which can be used in Controller/Views to get the current user information
public class GlobalSettings : IGlobalSettings
{
private IHttpContextAccessor _accessor;
public GlobalSettings(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public string RefNo
{
get
{
return GetValue(_accessor.HttpContext.User, "employeeid");
}
}
public string SAMAccount
{
get
{
return GetValue(_accessor.HttpContext.User, ClaimTypes.WindowsAccountName);
}
}
public string UserName
{
get
{
return GetValue(_accessor.HttpContext.User, ClaimTypes.Name);
}
}
public string Role
{
get
{
return GetValue(_accessor.HttpContext.User, ClaimTypes.Role);
}
}
private string GetValue(ClaimsPrincipal principal, string key)
{
if (principal == null)
return string.Empty;
return principal.FindFirstValue(key);
}
}
Example Usage in controller after DI:
var currentUser = GlobalSettings.SAMAccount;
Please note that you need to inject HttpContextAccessor in ConfigureServices method in Startup.cs
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();