I want to develop a SaaS app and deploy it on Azure. Because the business logic will be not so complex so I want to use as a starter kit JHipster. The app will be have two types of users: the "office" users which want to sign into the app using Office 365 account and the "normal" users which want to use their social accounts like Google or Facebook or simply create a new local account. All accounts should be managed by Azure and no password should be stored in our database.
First question is about Azure: which type of AAD should I use? B2B or B2C? Or a mix of both?
Second about JHipster: is it possible to configure JHipster to authenticate users against AAD? Which option should I select in the question about authentication creating the JHipster's app?
Third about Azure: it would be nice if the "office" user could add our SaaS app to the list of apps in the Office 365 main screen. Is it possible?
I have only "on-premise" experience, so maybe my questions are simple but these are my first steps into any clouds, in this case into Azure.
Regards,
Jacek
Pre Requisites: You need to have registered your app in the azure tenent and obtain a client id and secret.
Register App
In your application.yml file settings like these should connect you to azure ad.
# ===================================================================
# OpenID Connect Settings. Default settings are for Azure
# ===================================================================
security:
oauth2:
client:
access-token-uri: https://login.microsoftonline.com/common/oauth2/token
user-authorization-uri: https://login.microsoftonline.com/common/oauth2/authorize
client-id: <<yourclientid>>
client-secret: <<yourregistry>>
client-authentication-scheme: query
preEstablishedRedirectUri: http://localhost:8885/login
useCurrentUri: false
resource:
user-info-uri: https://graph.windows.net/me?api-version=1.6
id: https://graph.windows.net/
You will need to update the UserService class method getUser() to pull down the correct information.
private static User getUser(Map<String, Object> details) {
User user = new User();
user.setId((String) details.get("userPrincipalName"));
user.setLogin(((String) details.get("userPrincipalName")).toLowerCase());
if (details.get("givenName") != null) {
user.setFirstName((String) details.get("givenName"));
}
if (details.get("surname") != null) {
user.setLastName((String) details.get("surname"));
}
if (details.get("displayName") != null) {
user.setDisplayName((String) details.get("displayName"));
}
if (details.get("email_verified") != null) {
user.setActivated((Boolean) details.get("email_verified"));
}
if (details.get("userPrincipalName") != null) {
user.setEmail(((String) details.get("userPrincipalName")).toLowerCase());
}
if (details.get("langKey") != null) {
user.setLangKey((String) details.get("langKey"));
} else if (details.get("locale") != null) {
String locale = (String) details.get("locale");
if (locale.contains("-")) {
String langKey = locale.substring(0, locale.indexOf("-"));
user.setLangKey(langKey);
} else if (locale.contains("_")) {
String langKey = locale.substring(0, locale.indexOf("_"));
user.setLangKey(langKey);
}
}
if (details.get("thumbnailPhoto#odata.mediaEditLink") != null) {
user.setImageUrl((String) details.get("thumbnailPhoto#odata.mediaEditLink"));
}
user.setActivated(true);
return user;
}
Isn't the call to the graph API a GET instead of POST? Did this get changed on later jhipster releases? If so then some more work needs to be done to change the operation. In addition, I don't think the user-info-uri: https://graph.windows.net/me?api-version=1.6 endpoint gives you user roles (AD groups)you would have to make a second call. This of course depends of how your IDP was configured internally.
Related
I have a web api application which I allow an access to only authorized user.
I do it by using attribute [Authorize] with controllers
Can I restrict from accessing the application a particular user with a given username even though he/she's in Azure AD?
Can I restrict from accessing the application a particular user with a given username even though he/she's in Azure AD?
What you need is to create a policy and check current user against this policy whenever you want.
There're two ways to do that.
Use a magic string to configure policy (e.g. [Authorize(policy="require_username=name")]), and then create a custom policy provider to provide the policy dynamically. For more details, see https://learn.microsoft.com/en-us/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-2.2
Create a static policy and use a custom AuthorizeFilter to check whether current user is allowed.
Since this thread is asking "Restricting Azure AD users from accessing web api controller", I prefer to the 2nd way.
Here's an implementation for the 2nd approach. Firstly, let's define a policy of requirename:
services.AddAuthorization(opts =>{
opts.AddPolicy("requirename", pb => {
pb.RequireAssertion(ctx =>{
if(ctx.User==null) return false;
var requiredName = ctx.Resource as string;
return ctx.User.HasClaim("name",requiredName);
});
});
});
And to check against this policy, create a custom AuthorizeFilter as below:
public class RequireNameFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public string Name{get;set;}
public RequireNameFilterAttribute(string name) { this.Name = name; }
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var user= context.HttpContext.User;
if(user==null){
context.Result = new ChallengeResult();
return;
}
var authZService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var result= await authZService.AuthorizeAsync(user, this.Name, "requirename");
if (!result.Succeeded) {
context.Result = new ForbidResult();
}
}
}
Finally, whenever you want to deny users without required names, simply decorate the action method with a RequireNameFilter(requiredName) attribute:
[RequireNameFilter("amplifier")]
public string Test()
{
return "it works";
}
[Edit]
AAD can restrict Azure AD users from accessing web api controller on an Application level. But cannot disallow an user to access a Controller API (API level).
Here's how-to about restricting Azure AD users on an Application Level
Login your Azure portal:
Choose an Activity Directory (e.g. Default Directory)
Click [Enterprise applications]
Choose the application you want to restrict (e.g. AspNetCore-Quickstart)
Select [Properties], Change the [User assignment required] to Yes
Select [Users and groups], Add/Relete users for this application as you need :
Be aware Azure AD is actually an Indentity Provider. This approach only works for the entire application. It's impossible to allow some user to access the App but disallow him to access a specific controller without coding/configuring the Application. To do that, we have no choice but to authorize uses within the application.
I published a web app to Azures App Services. I used the App Service's Authentication/Authorization feature to provide security. I successfully added Active Directory features to my web service (and desktop client). It seemed to work very well. Couldn't access data from a browser or desktop client without signing in to the AD.
This was all before I added the [Authorize] attribute to any of the controllers in the API!
So, what will [Authorize] do (or add) to security in my web api. It seems to already be locked up by configuring the Authentication/Authorization features of the web app in Azure.
So, what will [Authorize] do (or add) to security in my web api.
Using ILSpy, you could check the source code about AuthorizeAttribute under System.Web.Mvc.dll. The core code for authorization check looks like this:
protected virtual bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated)
{
return false;
}
if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name, StringComparer.OrdinalIgnoreCase))
{
return false;
}
if (_rolesSplit.Length > 0)
{
string[] rolesSplit = _rolesSplit;
IPrincipal principal = user;
if (!rolesSplit.Any(principal.IsInRole))
{
return false;
}
}
return true;
}
The main process would check httpContext.User.Identity.IsAuthenticated, then check whether the current user name, user role is authorized or not when you specifying the allowed Users,Roles.
For Authentication and authorization in Azure App Service(Easy Auth) which is implemented as a native IIS module. Details you could follow Architecture of Azure App Service Authentication / Authorization.
It seemed to work very well. Couldn't access data from a browser or desktop client without signing in to the AD.
This was all before I added the [Authorize] attribute to any of the controllers in the API!
Based on your description, I assumed that you set Action to take when request is not authenticated to Log in with Azure Active Directory instead of Allow Anonymous requests (no action) under your Azure Web App Authentication/Authorization blade.
Per my understanding, you could just leverage App Service Authentication / Authorization which provides built-in authentication and authorization support for you without manually adding middleware in your code for authentication. App service authentication would validate the request before your code can process it. So, for additional custom authorization check in your code, you could define your custom authorize class which inherits from AuthorizeAttribute to implement your custom processing.
public class CustomAuthorize : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//TODO:
}
protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
{
//TODO:
}
}
Then, decorate the specific action(s) or controller(s) as follows:
[CustomAuthorize]
public class UsersController : Controller
{
//TODO:
}
App Service's Authentication/Authorization feature is Based on IIS Level. [Authorize] attribute is based on our code level. Both of this can do Authentication, if you used both of them, it means that there are two levels of authentication in your web app.
Here is a picture that helps you understand them:
I have taken this sample from github to attempt to use IdentityServer4 and Azure AD for authentication.
While I have it working and returning a token, it seems that the claims that I would have expected to receive from Azure AD are not included in the token(s) issued through IdentityServer.
It could be that this is intentional and that I have misunderstood this flow, but I was hoping that the roles that a user is assigned through Azure AD (plus the tenant ID and other useful 'bits' from the Azure token) would be able to be included in the tokens issued to the client.
Would anyone be able to shed some light on this for me? I can paste code in here but the link to the github code is pretty much the same as what I am using.
I was trying to do the same thing, and managed to eventually piece bits together from looking at the IS4 docs, Github and StackOverflow.
You need to configure a new instance of IProfileService (Docs) in order to tell IdentityServer4 which additional claims for a user's identity (obtained from Azure AD in your case) you want to be passed back to the client.
An example might look like this:
public class CustomProfileService : IProfileService
{
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
// Get the 'upn' claim out from the identity token returned from Azure AD.
var upnClaim = context.Subject.FindFirst(c => c.Type == ClaimTypes.Upn);
// Get 'firstname' and 'givenname' claims from the identity token returned from Azure AD.
var givenNameClaim = context.Subject.FindFirst(c => c.Type == ClaimTypes.GivenName);
var surNameClaim = context.Subject.FindFirst(c => c.Type == ClaimTypes.Surname);
// Add the retrieved claims into the token sent from IdentityServer to the client.
context.IssuedClaims.Add(upnClaim);
context.IssuedClaims.Add(givenNameClaim);
context.IssuedClaims.Add(surNameClaim);
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true;
return Task.CompletedTask;
}
}
You will then need to register this service in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
// Register the new profile service.
.AddProfileService<CustomProfileService>();
}
Finally, inside of your AccountController.cs (within the IdentityServer4 project - I'm assuming you already have this, see here for a starter setup if not), you need to add the following to ExternalLoginCallback():
[HttpGet]
public async Task<IActionResult> ExternalLoginCallback()
{
//...
// this allows us to collect any additonal claims or properties
// for the specific protocols used and store them in the local auth cookie.
// this is typically used to store data needed for signout from those protocols.
var additionalLocalClaims = new List<Claim>();
// ADD THIS LINE TO TELL IS4 TO ADD IN THE CLAIMS FROM AZURE AD OR ANOTHER EXTERNAL IDP.
additionalLocalClaims.AddRange(claims);
//...
}
Hope this helps.
I recently got started working with B2C. I managed to get their sample application/API using MSAL and an API working with my own tenant.
Now I wanted to:
Figure out how I can run this sample without using an API. The sample uses Scopes to get read/write access to the API. If I remove the references to the API from the app, it no longer works. Surely there should be some way to authenticate to B2C without requiring an API? This is not really important to my application but I'm mostly curious if the webservice HAS to be there as part of the auth-process?
Communicate with Graph Api (Windows or Microsoft Graph?). The sample MS provides uses ADAL and some console application. I cannot find a sample that uses MSAL, so I am having trouble incorporating it into my own application. Is it now possible to call Graph API using MSAL? If it is, is there some documentation on how to do this somewhere?
I tried simply following the docs above and registering an app/granting it permissions. Then putting the client ID/key into my own application (the MSAL one from the first sample), but then I just get a message from B2C that looks like:
Correlation ID: 01040e7b-846c-4f81-9a0f-ff515fd00398
Timestamp: 2018-01-30 10:55:37Z
AADB2C90068: The provided application with ID '9cd938c6-d3ed-4146-aee5-a661cd7d984b' is not valid against this service. Please use an application created via the B2C portal and try again.
It's true that it's not registered via the B2C portal, but that is what the instructions say; to register it in the B2C tenant under App Registrations, not the B2c portal.
The Startup class where all the magic happens looks like:
public partial class Startup
{
// App config settings
public static string ClientId = ConfigurationManager.AppSettings["ida:ClientId"];
public static string ClientSecret = ConfigurationManager.AppSettings["ida:ClientSecret"];
public static string AadInstance = ConfigurationManager.AppSettings["ida:AadInstance"];
public static string Tenant = ConfigurationManager.AppSettings["ida:Tenant"];
public static string RedirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
public static string ServiceUrl = ConfigurationManager.AppSettings["api:TaskServiceUrl"];
public static string ApiIdentifier = ConfigurationManager.AppSettings["api:ApiIdentifier"];
public static string ReadTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:ReadScope"];
public static string WriteTasksScope = ApiIdentifier + ConfigurationManager.AppSettings["api:WriteScope"];
public static string[] Scopes = new string[] { ReadTasksScope, WriteTasksScope };
// B2C policy identifiers
public static string SignUpSignInPolicyId = ConfigurationManager.AppSettings["ida:SignUpSignInPolicyId"];
public static string EditProfilePolicyId = ConfigurationManager.AppSettings["ida:EditProfilePolicyId"];
public static string ResetPasswordPolicyId = ConfigurationManager.AppSettings["ida:ResetPasswordPolicyId"];
public static string DefaultPolicy = SignUpSignInPolicyId;
// OWIN auth middleware constants
public const string ObjectIdElement = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
// Authorities
public static string Authority = String.Format(AadInstance, Tenant, DefaultPolicy);
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
// Generate the metadata address using the tenant and policy information
MetadataAddress = String.Format(AadInstance, Tenant, DefaultPolicy),
// These are standard OpenID Connect parameters, with values pulled from web.config
ClientId = ClientId,
RedirectUri = RedirectUri,
PostLogoutRedirectUri = RedirectUri,
// Specify the callbacks for each type of notifications
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = OnRedirectToIdentityProvider,
AuthorizationCodeReceived = OnAuthorizationCodeReceived,
AuthenticationFailed = OnAuthenticationFailed,
},
// Specify the claims to validate
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name"
},
// Specify the scope by appending all of the scopes requested into one string (seperated by a blank space)
Scope = $"{OpenIdConnectScopes.OpenId} {ReadTasksScope} {WriteTasksScope}"
}
);
}
/*
* On each call to Azure AD B2C, check if a policy (e.g. the profile edit or password reset policy) has been specified in the OWIN context.
* If so, use that policy when making the call. Also, don't request a code (since it won't be needed).
*/
private Task OnRedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var policy = notification.OwinContext.Get<string>("Policy");
if (!string.IsNullOrEmpty(policy) && !policy.Equals(DefaultPolicy))
{
notification.ProtocolMessage.Scope = OpenIdConnectScopes.OpenId;
notification.ProtocolMessage.ResponseType = OpenIdConnectResponseTypes.IdToken;
notification.ProtocolMessage.IssuerAddress = notification.ProtocolMessage.IssuerAddress.ToLower().Replace(DefaultPolicy.ToLower(), policy.ToLower());
}
return Task.FromResult(0);
}
/*
* Catch any failures received by the authentication middleware and handle appropriately
*/
private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
// Handle the error code that Azure AD B2C throws when trying to reset a password from the login page
// because password reset is not supported by a "sign-up or sign-in policy"
if (notification.ProtocolMessage.ErrorDescription != null && notification.ProtocolMessage.ErrorDescription.Contains("AADB2C90118"))
{
// If the user clicked the reset password link, redirect to the reset password route
notification.Response.Redirect("/Account/ResetPassword");
}
else if (notification.Exception.Message == "access_denied")
{
notification.Response.Redirect("/");
}
else
{
notification.Response.Redirect("/Home/Error?message=" + notification.Exception.Message);
}
return Task.FromResult(0);
}
/*
* Callback function when an authorization code is received
*/
private async Task OnAuthorizationCodeReceived(AuthorizationCodeReceivedNotification notification)
{
// Extract the code from the response notification
var code = notification.Code;
string signedInUserID = notification.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;
TokenCache userTokenCache = new MSALSessionCache(signedInUserID, notification.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase).GetMsalCacheInstance();
ConfidentialClientApplication cca = new ConfidentialClientApplication(ClientId, Authority, RedirectUri, new ClientCredential(ClientSecret), userTokenCache, null);
try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, Scopes);
}
catch (Exception ex)
{
//TODO: Handle
throw;
}
}
}
With regards to #2, you can only use the Azure AD Graph API with an Azure AD B2C directory, as noted in the "Azure AD B2C: Use the Azure AD Graph API" article.
Here is how (which I have copied from a previous answer)...
Azure AD B2C issues tokens using the Azure AD v2.0 endpoint:
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
The Azure AD Graph API requires tokens that are issued using the Azure AD v1.0 endpoint:
https://login.microsoftonline.com/{tenant}/oauth2/token
At design-time:
Register the web application using the Azure AD B2C portal.
Register the web application using the Azure AD portal and grant the Read directory data permission.
At runtime:
The web application redirects the end user to the Azure AD B2C v2.0 endpoint for sign-in. Azure AD B2C issues an ID token containing the user identifier.
The web application acquires an access token from the Azure AD v1.0 endpoint using the application credentials that were created at design-time in step 2.
The web application invokes the Azure AD Graph API, passing the user identifier that was received in step 1, with the access token that was issued in step 2, and queries the user object, etc.
This answer is just addressing question #1, I am unsure on #2 other than it is not publicly documented as far as I know.
B2C does not require you to use an API/service when building your app. In the sample you're looking at it's using 2 libraries to do slightly different (but related) things.
First, it's using an OWIN middleware module to help facilitate the authentication piece. This helps your app identify who a user is, but does not authorize your app to do perform actions or access data on their behalf. This library will net you a session with the end user and basic information about them as well as a authorization code you can use later on.
The other library being used is MSAL. MSAL is a client library for token acquisition and management. In this case after the initial authentication takes place using the aforementioned middleware, MSAL will use the authorization code to get access and refresh tokens that your app can use to call APIs. This is able to take place without end user interaction because they would have already consented to the app (and you would've configured the permissions your app needed). MSAL then manages the refresh and caching of these tokens, and makes them accessible via AcquireTokenSilent().
In order to remove the API functionality from the sample app, you'd need to do a bit more than just remove the scope. Specifically, eliminating the code in the TaskController.cs that is trying to call APIs, remove most usage of MSAL, and likely a few more things. This sample implements a Web App only architecture (Warning: it's for Azure AD not Azure AD B2C. The code is very similar, but would require a bit of modification).
I know that local debugging using tokens is possible using http://www.systemsabuse.com/2015/12/04/local-debugging-with-user-authentication-of-an-azure-mobile-app-service/. Would it be possible to go to thesite.com/.auth/login/aad and login and use that cookie for localhost (for testing the web app - not the mobile app)?
I am currently using the .auth/login/aad cookie to authenticate Nancy. I do by generating a ZumoUser out of the Principal.
Before.AddItemToEndOfPipeline(UserToViewBag);
and
internal static async Task<Response> UserToViewBag(NancyContext context, CancellationToken ct)
{
var principal = context.GetPrincipal();
var zumoUser = await ZumoUser.CreateAsync(context.GetPrincipal());
context.ViewBag.User = zumoUser;
context.Items["zumoUser"] = zumoUser;
var url = context.Request.Url;
if (zumoUser.IsAuthenticated)
{
_logger.DebugFormat("{0} requested {1}", zumoUser, url.Path);
}
else
{
_logger.DebugFormat("{0} requested {1}", "Anonymous", url.Path);
}
return null;
}
Yes. You need to read "the book" as it is a complex subject. The book is available open source at http://aka.ms/zumobook and the content you want is in Chapter 2.
Would it be possible to go to thesite.com/.auth/login/aad and login and use that cookie for localhost (for testing the web app - not the mobile app)?
No, this is impossible. The JWT token verification is based on the stand protocol(OpenId connect or Oauth 2) we can follow. But there is no official document or SDK about the the cookie issued by the Easy Auth verification.