I want to set an expiry time for my web app so that after 1 hour the user will automatically be logged out. It seems that azure now have a new feature called 'conditional access'. Firstly I have to have a premium account (so yet more money) and secondly I cant find anything in the documentation that shows how to use it to log someone out after a specified time. Has anyone used this feature yet ? how can this be done ?
can anyone help ?
Assuming that you are using OpenID Connect and Cookie authentication middleware to protect your web app, for your requirement, I assumed that you could add a custom claim named loggedTicks and check the time interval under the OnValidateIdentity of CookieAuthenticationProvider, then explicitly invoke the sign out operation against your web application and AAD. Here is the code snippet, you could refer to it:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions() {
ExpireTimeSpan=TimeSpan.MaxValue,
Provider = new CookieAuthenticationProvider()
{
OnValidateIdentity = ctx => {
var loggedClaim=ctx.Identity.FindFirst("loggedTicks")?.Value;
if (loggedClaim != null)
{
var loggedDateTime = new DateTime(long.Parse(loggedClaim), DateTimeKind.Utc);
if (loggedDateTime.AddHours(1) < DateTime.UtcNow)
{
ctx.RejectIdentity();
ctx.OwinContext.Authentication.SignOut(OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);
}
}
return Task.FromResult(0);
}
}
});
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
RedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = context =>
{
context.HandleResponse();
context.Response.Redirect("/Error?message=" + context.Exception.Message);
return Task.FromResult(0);
},
SecurityTokenValidated = async (x) =>
{
var identity = x.AuthenticationTicket.Identity;
//add a additional claim which represents the current user logged UTC time ticks
identity.AddClaim(new System.Security.Claims.Claim("loggedTicks", DateTime.UtcNow.Ticks.ToString()));
await Task.FromResult(0);
}
}
});
}
Related
I want to enable Multitenant Authentication. My Code is in ASP.Net Webforms and Here is the StartUp.cs file code.
public partial class Startup
{
const string MSATenantId = "XXXXXXXXXXXXXXX";
private static string clientId = ConfigurationManager.AppSettings["ida:ClientID"];
private static string aadInstance = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:AADInstance"]);
private static string authority = aadInstance + "common";
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions { });
// instead of using the default validation (validating against a single issuer value, as we do in line of business apps),
// we inject our own multitenant validation logic
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = new List<string>()
{
"https://sts/windows.net/XXXXXXXXXXXX"
}
// If the app needs access to the entire organization, then add the logic
// of validating the Issuer here.
// IssuerValidator
},
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
//if (context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value != MSATenantId)
//{
// context.HandleResponse();
// context.Response.Redirect("InvalidUser.aspx");
//}
// If your authentication logic is based on users
return Task.FromResult(0);
},
AuthenticationFailed = (context) =>
{
// Pass in the context back to the app
context.HandleResponse();
// Suppress the exception
return Task.FromResult(0);
}
},
});
// This makes any middleware defined above this line run before the Authorization rule is applied in web.config
app.UseStageMarker(PipelineStage.Authenticate);
}
//private Task OnSecurityTokenValidatedAsync(SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
//{
// // Make sure that the user didn't sign in with a personal Microsoft account
// if (notification.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value == MSATenantId)
// {
// notification.HandleResponse();
// notification.Response.Redirect("/Account/UserMismatch");
// }
// return Task.FromResult(0);
//}
}
I want only the user with the MSATenantId should able to access the application for that I have read there are multiple ways I have tried below two though both are not working:
In this the application doesn't redirect to the Home page
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = new List<string>()
{
"https://sts/windows.net/XXXXXXXXXX"
}
// If the app needs access to the entire organization, then add the logic
// of validating the Issuer here.
// IssuerValidator
},
In this it doesn't redirect to invalid page.
SecurityTokenValidated = (context) =>
{
if (context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value != MSATenantId)
{
context.HandleResponse();
context.Response.Redirect("InvalidUser.aspx");
}
If your authentication logic is based on users
return Task.FromResult(0);
},
Am I missing anything or do I need to add something in the above scenarios. I want to just test with one Tenant first and then I'll add more tenant.
Also, how does the 1 and 2 are different ?
This works if I don't use any of the above option. I am able to login with Azure account.
Your question has been resolved, add it as the answer to the end of the question.
Your issuer is set incorrectly, you should change it to: https://sts.windows.net/XXXXXXXXXXXX/.
I am able to resolve the issue. Issue was this url was incorrect https://sts/windows.net/XXXXXXXXXXXX
The correct URL is - https://sts.windows.net/XXXXXXXXXXXX/
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = new List<string>()
{
"https://sts.windows.net/XXXXXXXXXX/"
}
// If the app needs access to the entire organization, then add the logic
// of validating the Issuer here.
// IssuerValidator
},
I'm building an Azure AD authentication which works fine, the only issue is that if a user clicks on a link that looks like host.com/xxx/bbb if they're not authenticated then they get redirected to root.
I need them to still be redirected to the original URL they entered in the browser. Can can this be achieved? Below is a snippet of the code I use in app startup:
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = XXX,
Authority = string.Format("https://login.microsoftonline.com/{0}", YYY,
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = async n => { await Task.Run(() => SetRedirectUrl(n)); }
}
});
}
private static void SetRedirectUrl(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.ProtocolMessage.RedirectUri = notification.Request.Uri.AbsoluteUri;
}
None of the properties of OwinRequest contain a full path I'm looking for. I've also tried looking at
HttpContext.Current.Request.Url
but this also does not have the full address.
I need them to still be redirected to the original URL they entered in
the browser
You need to set the SignIn() method in AccountController.cs. You could use “Request.UrlReferrer.ToString();” to get the entered url:
public void SignIn()
{
// var host = Request.UserHostName.ToString();
var ori = Request.UrlReferrer.ToString();
var index = ori.LastIndexOf('/');
var action = ori.Substring(index);
// Send an OpenID Connect sign-in request.
if (!Request.IsAuthenticated)
{
HttpContext.GetOwinContext().Authentication.Challenge(new AuthenticationProperties { RedirectUri = action },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
}
}
I have got a basic Identity Server setup as per the UI sample project instructions on GitHub. I have it set it up to use Windows authentication with our on site AD. This is working beautifully.
My issue is with adding the users AD groups to the claims. As per the sample project I have enabled the IncludeWindowsGroups option. Which seems to be adding the claims to the ClaimsIdentity. However, on my MVC client, when I print out the claims I only ever get the same 4. They are sid, sub, idp and name. I have tried adding other claims but I can never get any others to show up.
I have the following as my Client Setup:
return new List<Client>
{
// other clients omitted...
// OpenID Connect implicit flow client (MVC)
new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Implicit,
// where to redirect to after login
RedirectUris = { "http://localhost:5002/signin-oidc" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile
},
RequireConsent = false
}
};
Hopefully I am just missing something simple but I am struggling for ideas now, so any pointers would be much appreciated.
I managed to get this working with a few changes, beyond setting IncludeWindowsGroups = true in the IdentityServer4 project. Note that I downloaded the IdentityServer4 UI quickstart as of the 2.2.0 tag
Per this comment in GitHub, I modified ExternalController.cs in the quickstart UI:
// this allows us to collect any additonal claims or properties
// for the specific prtotocols 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>();
var roleClaims = claims.Where(c => c.Type == JwtClaimTypes.Role).ToList();
if (roleClaims.Count > 0)
{
additionalLocalClaims.AddRange(roleClaims);
}
I then created a profile service to copy the claims from Windows Auth into the token being sent back:
public class ProfileService : IProfileService
{
private readonly string[] _claimTypesToMap = {"name", "role"};
public Task GetProfileDataAsync(ProfileDataRequestContext context)
{
foreach (var claimType in _claimTypesToMap)
{
var claims = context.Subject.Claims.Where(c => c.Type == claimType);
context.IssuedClaims.AddRange(claims);
}
return Task.CompletedTask;
}
public Task IsActiveAsync(IsActiveContext context)
{
context.IsActive = true; //use some sort of actual validation here!
return Task.CompletedTask;
}
}
and registered with IdentityServer4 in Startup.cs
services
.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(StaticConfig.GetIdentityResources())
.AddInMemoryApiResources(StaticConfig.GetApiResources())
.AddInMemoryClients(StaticConfig.GetClients())
.AddTestUsers(StaticConfig.GetUsers())
.AddProfileService<ProfileService>();
In my client config in IdentityServer4, I set user claims to be included in the Id token. I found that if I tried to map the claims in the callback to UserInfo, that context was lost in IdentityServer4, so the claims wouldn't map.
public static class StaticConfig
{
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
...
AlwaysIncludeUserClaimsInIdToken = true,
...
}
}
}
}
Finally, in Startup.cs for the client website, I did not setup the UserInfo callback; I just made sure that my name and role claims were mapped. Note that if your profile service returns any other claim types, you need to manually map them with a call to a helper method on options.ClaimActions.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.SaveTokens = true;
options.ResponseType = "code id_token";
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
};
//map any other app-specific claims we're getting from IdentityServer
options.ClaimActions.MapUniqueJsonKey("someotherclaimname", "someotherclaimname");
};
Summary:
Our Universal Windows App single-tenant client uses an ASP.NET Web API 2 as a proxy for single-sign on for various Microsoft Office 365 APIs. We use Active Directory for server authentication and the on-behalf-of single sign-on model in our server to exchange tokens for the Office 365 APIs.
Problem:
We have updated a permission scope in Azure for the Office 365 API and the user is not prompted to authorize permission for the new scope, nor is the new scope appearing on NEW tokens. What needs to be done to DETECT and ALLOW our users to authorize new permission scopes?
Additional Details:
Our server is hosted in MSFT Azure App Services. I understand the manifest in Azure is auto-generated and does not need to be manually updated to reflect the updated permission scope?
When the user first logs into the UWP app, they consent to single sign-on permissions associated with the server (eg. Mail.ReadWrite, etc.) which works fine. However, the user consent prompt does not show up again, even after I’ve removed both the client and server apps from my list of consented to apps using
We use the WebTokenRequest and WebAuthenticationCoreManager libraries in the client to get the token for the server. I have also tried using WebAuthenticationBroker (which is not the correct method for our sign-on architecture) and the ADAL library in our client. None of these libraries are prompting for the updated permission.
I have also tried adding wtf.Properties.Add("prompt", "consent"); to our WebTokenRequest to force the user to reapprove permissions. This does not work.
I have also tried restarting the App Service in Azure. This does nothing.
UPDATED 11/10/16:
Following is relevant code I've pulled from our app architecture which may help. Additionally, our server utilizes ADAL version 2.24.304111323.
In our UWP app:
public class AppAuth
{
WebTokenRequestResult result;
WebAccount acc;
async Task<WebTokenRequestResult> GetTokenAsync(WebTokenRequestPromptType promptType = WebTokenRequestPromptType.Default)
{
var wtr = new WebTokenRequest(
provider: "https://login.windows.net",
scope: "",
clientId: appClientId,
promptType: promptType
);
wtr.Properties.Add("authority", "https://login.windows.net");
wtr.Properties.Add("resource", azureWebsiteUrl);
if (promptType != WebTokenRequestPromptType.ForceAuthentication)
{
result = (acc == null) ?
await WebAuthenticationCoreManager.GetTokenSilentlyAsync(wtr) :
await WebAuthenticationCoreManager.GetTokenSilentlyAsync(wtr, acc);
}
if (promptType == WebTokenRequestPromptType.ForceAuthentication ||
result?.ResponseStatus == WebTokenRequestStatus.UserInteractionRequired)
{
result = (acc == null) ?
await WebAuthenticationCoreManager.RequestTokenAsync(wtr) :
await WebAuthenticationCoreManager.RequestTokenAsync(wtr, acc);
}
return result;
}
}
In our server:
public partial class Startup
{
public void ConfigureAuth(IAppBuilder app)
{
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
Tenant = ConfigurationManager.AppSettings["ida:Tenant"],
TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = true,
ValidateIssuer = false,
ValidAudience = ConfigurationManager.AppSettings["ida:Audience"]
}
});
}
}
public class TokenChange
{
protected AdUser _user;
private UserAssertion _assertion;
private static string _aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string _tenant = ConfigurationManager.AppSettings["ida:Tenant"];
private static string _clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string _appKey = ConfigurationManager.AppSettings["ida:AppKey"];
private string _accessToken;
public AuthenticationResult AuthResult { get; set; }
public AdalException AuthException { get; set; }
private string _emailAddress;
private HttpClient _httpClient;
public bool Authenticate()
{
_accessToken = null;
if (ClaimsPrincipal.Current.Identity.IsAuthenticated)
{
var bootstrapContext = ClaimsPrincipal.Current.Identities.First().BootstrapContext
as System.IdentityModel.Tokens.BootstrapContext;
if (bootstrapContext != null)
{
Claim subject = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier);
var upn = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn);
var email = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email);
var userName = upn != null ? upn.Value : email?.Value;
_emailAddress = ClaimsPrincipal.Current.Identity.Name;
var userNameClaim = ClaimsPrincipal.Current.FindFirst("name");
_fullName = userNameClaim != null ? userNameClaim.Value : String.Empty;
_accessToken = bootstrapContext.Token;
_assertion = new UserAssertion(_accessToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
}
}
return _accessToken != null;
}
public bool GetAccess(string apiResource)
{
bool gotAccess = false;
AuthResult = null;
AuthException = null;
if (_accessToken != null || Authenticate())
{
ClientCredential clientCred = new ClientCredential(_clientId, _appKey);
string authority = String.Format(CultureInfo.InvariantCulture, _aadInstance, _tenant);
AuthenticationContext authContext = new AuthenticationContext(authority);
bool retry = false;
int retryCount = 0;
do
{
retry = false;
try
{
AuthResult = authContext.AcquireToken(apiResource, clientCred, _assertion);
}
catch (AdalException ex)
{
AuthException = ex;
if (ex.ErrorCode == "temporarily_unavailable")
{
retry = true;
retryCount++;
Thread.Sleep(500);
}
else
{
throw (ex);
}
}
} while ((retry == true) && (retryCount < 1));
if (AuthResult != null && AuthResult.AccessToken != null)
{
gotAccess = true;
}
}
return gotAccess;
}
Based on the description, you were developing an single tenant application which calling the downstream web API(Office 365 API) in your web API.
If you were using the cache to acquire the token in your web API, it will not acquire the new token unless the token is expired. And in this scenario, there is no need to consent/reconsent to update the permission.
Please ensure that you web API is acquire the token from new request instead of cache. If you were using the DbTokenCache, you can clear the cache by deleting the token cache records in PerWebUserCaches table in the database.
Note
In the describing scenario above, since the downstream web API(Office 365 API) get the token using the token issued for your web API which require users sign-in. So only the delegated permission work in the scenario( scp claim in the token instead of roles).
I am using Azure AD to authenticate the users. I want to add few user claims specific to my application. Should I do it in Application_PostAuthenticateRequest` in global.asax ?. Is there a way I can cache my claims too ?
If you are using the ASP.NET OWIN middleware, there are specific notifications you can use for that purpose. Claims added in that way will end up in your session cookie, so that you won't have to repeat the claims augmentation logic in subsequent calls. See http://www.cloudidentity.com/blog/2015/08/26/augmenting-the-set-of-incoming-claims-with-the-openid-connect-and-oauth2-middleware-in-katana-3-x/ for details.
BTW you can add your custom cliams but you cannot override the existing claims added by the Azure AD (what i have seen so far might be i am wrong). what you can do is to add the new cliams like this
AuthorizationCodeReceived = context =>
{
List<System.Security.Claims.Claim> allcustomClaims = new List<System.Security.Claims.Claim>();
allcustomClaims.Add(new System.Security.Claims.Claim("customClaim", "YourDefindedValue"));
context.AuthenticationTicket.Identity.AddClaims(allcustomClaims);
return Task.FromResult(0);
}`
and then you can get the claim anywhere in controller like
#{
var claimsIdentity = User.Identity as System.Security.Claims.ClaimsIdentity;
if (claimsIdentity != null)
{
var c = claimsIdentity.FindFirst("customClaim").Value;
}
}
You can augment the claims programmatically like this:
public async Task<ActionResult> AuthenticateAsync()
{
ClaimsPrincipal incomingPrincipal = System.Threading.Thread.CurrentPrincipal as ClaimsPrincipal;
if (incomingPrincipal != null && incomingPrincipal.Identity.IsAuthenticated == true)
{
ClaimsIdentity claimsIdentity = incomingPrincipal.Identity as ClaimsIdentity;
if (!claimsIdentity.HasClaim(ClaimTypes.Role, "Admin"))
{
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Admin", ClaimValueTypes.String, "AADGuide"));
var ctx = Request.GetOwinContext();
var authenticationManager = ctx.Authentication;
AuthenticateResult authResult = await authenticationManager.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType);
authenticationManager.SignIn(authResult.Properties,claimsIdentity);
}
}
return RedirectToAction("Index", "Start");
}
This solution relies on AuthenticationAsync method of AuthenticationManager to retrieve the original AuthenticationProperties. After retrieving the properties, call the SignIn method to persist the new ClaimsIdentity in the auth cookie.
If you're making use of:
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
...
This is how I managed to add additional custom claims using new OAuthBearerAuthenticationProvider:
app.UseWindowsAzureActiveDirectoryBearerAuthentication(
new WindowsAzureActiveDirectoryBearerAuthenticationOptions
{
// The id of the client application that must be registered in Azure AD.
TokenValidationParameters = new TokenValidationParameters { ValidAudience = clientId },
// Our Azure AD tenant (e.g.: contoso.onmicrosoft.com).
Tenant = tenant,
Provider = new OAuthBearerAuthenticationProvider
{
// In this handler we can perform additional coding tasks...
OnValidateIdentity = async context =>
{
try
{
// Retrieve user JWT token from request.
var authorizationHeader = context.Request.Headers["Authorization"].First();
var userJwtToken = authorizationHeader.Substring("Bearer ".Length).Trim();
// Get current user identity from authentication ticket.
var authenticationTicket = context.Ticket;
var identity = authenticationTicket.Identity;
// Credential representing the current user. We need this to request a token
// that allows our application access to the Azure Graph API.
var userUpnClaim = identity.FindFirst(ClaimTypes.Upn);
var userName = userUpnClaim == null
? identity.FindFirst(ClaimTypes.Email).Value
: userUpnClaim.Value;
var userAssertion = new UserAssertion(
userJwtToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
identity.AddClaim(new Claim(identity.RoleClaimType, "myRole"));
}
catch (Exception e)
{
throw;
}
}
}
});
For a full sample, check this blog post.