Grails session attributes are not flushed between two applications - session

I have two separate Grails applications that use the JASIG CAS Client Plugin. When my applications run, different users can authenticate against my CAS server (using the client plugin).
Authentication aside, I use a Grails Filter for authorization in both applications. A user may be authenticated, but I want to make sure that only certain users can only access the appropriate application.
Everything works, except for the below scenario:
I successfully authenticate against the CAS server with username "jack" and I am authorized to use Application A.
I close Application A and sign out of CAS.
I successfully authenticate against the CAS server with username "jill", but I am not authorized to use the Application B because the username from the session is still "jack"
Do I need to flush the session at any point when my applications initiate? If so, how can I do that? Here is the code for my Filter:
import edu.yale.its.tp.cas.client.filter.CASFilter
class SecurityFilters {
def filters = {
loginCheck(controller: '*', action: '*') {
before = {
def username = session?.getAttribute(CASFilter.CAS_FILTER_USER)?.toLowerCase()
if (username in grailsApplication.config.users) {
return true
} else {
render view: '/invalid_user', model: [username: username]
return false
}
}
}
}
}

Related

MsalClientException IDW10104 from GetAccessTokenForAppAsync

I have an ASP.NET Core Web API set up as App Service in Azure with an App Registration in our AzureAd
In appsettings.json I have (anonimized)
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "ourdomain.co.uk",
"TenantId": "n9n999n9-9999-nnnn-9n9n9-9n9n9n9n9n9",
"ClientId": "81933a15-157f-45b0-bc32-3d7d6d62f4a7",
"Audience": "https://ourdomain.co.uk/breathe.notifications-service",
"ClientSecret": "a6a6a6a~EEizqWNa8itAAAjcrycxnCtxaVgKTFx"
},
That app has an API permission in Azure Ad that allows me to call another app service, Audit. The audit service does not have any specific scopes defined but it does have an app role called Audit.Write
In the calling API i need to get a token to call audit so I run this code
var accessToken = await this.tokenAcquisition.GetAccessTokenForAppAsync(this.auditApiScope);
this.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
this.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
Note the call to GetAccessTokenForAppAsync rather than the more common GetAccessTokenForUserAsync
The scope string that I am passing is
https://ourdomain.co.uk/us.audit-service/.default
When I call GetAccessTokenForAppAsync it is failing with MSALException
IDW10104: Both client secret and client certificate cannot be null or
whitespace, and only ONE must be included in the configuration of the
web app when calling a web API. For instance, in the appsettings.json
file.
The client secret is in the AzureAd config, I am not specifying a certificate.
I now have this working and have two options but before I outline those I need to offer some extra background.
This Web Api and others we have created offer functionality to Azure Ad users and Azure B2C users. This functionality was first possible with Microsoft.Identity.Web 1.11.0 and we hjave been using 1.11.0 since it was released. However we always had an issue where we would generate thousands of exceptions because MSAL was getting confused ny which scheme to use.
We came across this blog post, Removing misleading IDX10501 logs when using multiple authentication schemes in ASP.NET Core 3.1 there is more detail in this github thread, https://github.com/oliviervaillancourt/blog/issues/3.
Our Startup.cs Configure Services looks like this
public void ConfigureServices(IServiceCollection services)
{
services.AddMicrosoftIdentityWebApiAuthentication(this.configuration)
.EnableTokenAcquisitionToCallDownstreamApi()
.AddInMemoryTokenCaches();
services.AddAuthentication()
.AddMicrosoftIdentityWebApi(this.configuration, "AzureAdB2C", "B2CScheme", true);
services.AddAuthentication("AzureAD_OR_AzureAdB2C")
.AddMicrosoftIdentityWebApi(
jwtBearerOptions =>
{
var azureAdB2CConfig = this.configuration.GetSection("AzureAdB2C");
jwtBearerOptions.ForwardDefaultSelector = context =>
{
var token = string.Empty;
if (context.Request.Headers.TryGetValue("Authorization", out var value))
{
string authorization = value;
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
}
if (token == null)
{
this.logger.LogInformation($"Cannot get the Token out of the Authorization header");
}
var jwtHandler = new JwtSecurityTokenHandler();
if (jwtHandler.CanReadToken(token))
{
var jwtToken = jwtHandler.ReadJwtToken(token);
var expectedB2CIssuer = $"{azureAdB2CConfig.GetValue<string>("Instance")}/{azureAdB2CConfig.GetValue<string>("TenantId")}/v2.0/";
if (string.Compare(jwtToken.Issuer, expectedB2CIssuer, true) == 0)
{
// Claim is from B2C so this request should be validated against the B2C scheme.
this.logger.LogInformation($"Request is with a B2C issued token so refer to B2CScheme. Token issuer: {jwtToken.Issuer} B2C Issuer: {expectedB2CIssuer}");
return "B2CScheme";
}
else
{
this.logger.LogInformation($"Request is not with a B2C issued token so refer to Bearer scheme. Token issuer: {jwtToken.Issuer} B2C Issuer: {expectedB2CIssuer}");
}
}
else
{
this.logger.LogInformation("Request token could not be read so refer to Bearer scheme");
}
return "Bearer";
};
},
identityOptions =>
{
var azureAdB2CConfig = this.configuration.GetSection("AzureAdB2C");
identityOptions.Instance = azureAdB2CConfig.GetValue<string>("Instance");
identityOptions.TenantId = "AzureAD_OR_AzureAdB2C";
identityOptions.ClientId = "AzureAD_OR_AzureAdB2C";
},
"AzureAD_OR_AzureAdB2C",
false);
services.AddControllers()
.AddNewtonsoftJson();
services.AddLogging(options =>
{
// hook the Console Log Provider
options.AddConsole();
options.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
// hook the Application Insights Provider
options.AddFilter<ApplicationInsightsLoggerProvider>(string.Empty, Microsoft.Extensions.Logging.LogLevel.Trace);
// pass the InstrumentationKey provided under the appsettings
options.AddApplicationInsights(this.configuration["APPINSIGHTS_INSTRUMENTATIONKEY"]);
});
}
The logic used by the ForwardDefaultSelector is what helps us work with multiple schemes and forward ASP.NET to the right scheme.
Now back to the answer.
If I remove the ForwardDefaultSelector I no longer get the IDW10104 however that is what we use to remopve all the extraneous exceptions schemes so that is not really going to be workable.
The only viable option is to move the Web Api from the latest version of Microsoft.Identity.Web 1.21.1 to 1.16.0. The issue that is causing us to get the exception was introduced in 1.16.1. I will raise an issue on the MSAL github for 1.16.1. We were previously using 1.11.0.

How to exchange JWT token for Credentials in Cognito Identity Pool in .NET Core 3.1 WebApi

Broad Overview: I am trying to create a .Net Core 3.1 WebApi backend that is authenticated against Amazon Cognito. I want to use the Amazon-hosted sign-in page(s) provided by Cognito. I want to leverage Cognito Identity Pool to provide temporary scoped credentials for users after they have logged in. I cannot figure out how to exchange the Cognito token to create the Credentials to call AWS services.
Technology Overview
.NET Core 3.1 WebApi
Amazon Cognito User Pool for initial authentication
Amazon Identity Pool for defining permissions (Roles) for logged in users
Deployed on AWS via API Gateway + Lambda using the AWS Serverless framework (basically CloudFormation)
Currently both of the following work:
Add [Authorize] attribute to a controller endpoint and access the URL in a browser. This re-directs me to the Cognito-hosted login page and, upon successful login, returns me back to the controller/endpoint and I am authorized.
Create a separate Client application and login to AWS Cognito. Pass the JWT token in the Authorization HTTP header when calling APIs from the client and the Authorization succeeds and API access is granted.
In both cases, the access to the API is permitted however the AmazonServiceClient instances that are created in the WebApi are granted the permissions associated with the Lambda function (which is the proper behavior).
Problem
I need to create AmazonServiceClients whose credentials match the Role defined by the Cognito Identity Pool.
To do this, I need to exchange token provided by logging into Cognito User Pool for temporary credentials in the Identity Pool.
Virtually ALL examples and documentation I can find on this process define how to manually login to Cognito using the API (not the hosted web UI), and then using the API response to create a CognitoUser and then get credentials from the Identity Pool using that user.
The closest (though super brief) documentation I can find to do what I need is from AWS here: https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/cognito-creds-provider.html
// Authenticate user through Facebook
string facebookToken = GetFacebookAuthToken();
// Add Facebook login to credentials. This clears the current AWS credentials
// and retrieves new AWS credentials using the authenticated role.
credentials.AddLogin("graph.facebook.com", facebookAccessToken);
While that example uses Facebook, conceptually it should be the same for any provider (Facebook, Google, Twitter, OpenId, etc.).
My Current Attempt
I have registered CognitoAWSCredentials as a Scoped service as it is user-specific and therefore should only exist as long as the API request session exists.
RegionEndpoint region = Configuration.GetAWSOptions().Region;
services.AddScoped(_ => new CognitoAWSCredentials(Settings.CognitoIdentityPoolId, region));
I have created an event handler that gets triggered when the OpenIdConnect event 'OnTokenValidated' is fired. This happens after I login to the Cognito hosted web UI and am redirected back to my API.
In this handler I can call:
CognitoAWSCredentials creds = services.BuildServiceProvider().GetRequiredService<CognitoAWSCredentials>();
creds.AddLogin( ... ??? ...);
(note: since I'm setting all this up in the Startup.ConfigureServices(IServiceCollection services) method, I am building an IServiceProvider instance each time authentication succeeds... which may be inefficient but I haven't figured out another way to access a scoped service inside the ConfigureServices method)
All this preamble to say that I cannot find a set of values for the AddLogin call which allow this test call to succeed:
ImmutableCredentials immCreds = creds.GetCredentials();
Relevant Data Structures
In the event handler where I can call AddLogin, I have access to: Microsoft.AspNetCore.Authentication.OpenIdConnect.TokenValidatedContext which in particular contains:
Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectMessage with:
access_token
id_token
refresh_token
System.IdentityModel.Tokens.Jwt.JwtSecurityToken with:
{
{
"alg": "RS256",
"kid": "**************************"
}. {
"at_hash": "**************************",
"sub": "**************************",
"email_verified": true,
"iss": "https://cognito-idp.ca-central-1.amazonaws.com/**************************",
"cognito:username": "**************************",
"nonce": "**************************",
"aud": "**************************",
"event_id": "**************************",
"token_use": "id",
"auth_time": 1595260191,
"exp": 1595263791,
"iat": 1595260191,
"email": "**************************"
}
}
I have tried using the iss value as the providerName in AddLogin, and either the access_token or id_token but neither work.
Does anyone know what I need to use for AddLogin in order for Cognito to create Identity Pool credentials for me based upon a JWT token from a Cognito User Pool login?
unless I missed it, I haven't seen documentation that states this, but even though all the Issuer fields on the various data structures include the 'https://', you need to strip it before using the Issuer as the providerName on the AddLogin call. ugh.
CognitoAWSCredentials creds = services.BuildServiceProvider().GetRequiredService<CognitoAWSCredentials>();
string shortIssuer = tokenValidatedContext.SecurityToken.Issuer;
if (shortIssuer.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) shortIssuer = shortIssuer.Substring("https://".Length);
if (shortIssuer.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)) shortIssuer = shortIssuer.Substring("http://".Length);
creds.AddLogin(shortIssuer, tokenValidatedContext.TokenEndpointResponse.IdToken);
now, the above code has a problem as the services.BuildServiceProvider(). part means the credentials object I modify isn't global (only local to the service provider I built here I think), but that's a different issue - just noting that in case anyone is copying this code.
services...<other authentication setup>...
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ClientId = Settings.CognitoClientId;
options.MetadataAddress = CognitoMetadataAddress;
options.ResponseType = OpenIdConnectResponseType.Code;
options.SaveTokens = true;
options.UsePkce = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidIssuers = new string[] { Settings.CognitoAuthority },
ValidateAudience = true,
ValidAudiences = new string[] { Settings.CognitoClientId }
};
options.Events = new OpenIdConnectEvents() {
OnTokenValidated = tokenValidatedContext => {
CognitoAWSCredentials creds = services.BuildServiceProvider().GetRequiredService<CognitoAWSCredentials>();
string shortIssuer = tokenValidatedContext.SecurityToken.Issuer;
if (shortIssuer.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase)) shortIssuer = shortIssuer.Substring("https://".Length);
if (shortIssuer.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase)) shortIssuer = shortIssuer.Substring("http://".Length);
creds.AddLogin(shortIssuer, tokenValidatedContext.TokenEndpointResponse.IdToken);
return Task.CompletedTask;
}
};
})
(some code removed to focus on specifically the OpenId Connect event and the CognitoAWSCredentials init)

Secured users created in grails integration test are unauthorized but bootstrapped ones are

I'm using Grails Spring Security Core and the Grails Spring Security REST plugin and I'm just starting to get things set up. I initialized the plugins with a User class and an Authority class (defaults) and went to write an integration test, following a guide I found on the Grails website.
It said to put the following in an integration test:
def "test a user with the role ROLE_BOSS is able to access /api/announcements url"() {
when: 'login with the sherlock'
RestBuilder rest = new RestBuilder()
def resp = rest.post("http://localhost:${serverPort}/api/login") {
accept('application/json')
contentType('application/json')
json {
username = 'sherlock'
password = 'elementary'
}
}
then:
resp.status == 200
resp.json.roles.find { it == 'ROLE_BOSS' }
}
I went ahead and did something similar and it worked with a bootstrapped User, but when I tried to do the exact same test with a User created in the test method itself, it would fail with a 401 HTTP response code.
The code I'm trying to run:
void "check get access token"() {
given:
RestBuilder rest = new RestBuilder()
new User(username: "securitySpecTestUserName", password: "securitySpecTestPassword").save(flush: true)
assert User.count == 2
when:
def resp = rest.post("http://localhost:${serverPort}/api/login") {
accept('application/json')
contentType('application/json')
json {
username = "securitySpecTestUserName"
password = "securitySpecTestPassword"
}
}
then:
resp.status == 200
}
Note that the User.count == 2 assertion passes because there is one User in Bootstrap.groovy and the one create in the test method.
Why does this work and pass with the bootstrapped User without any issues at all but not the one created in the method? Is there a way I can write this integration test so that I can test the /api/login endpoint included in the grails-spring-security-rest plugin in this way?
The User you create in the given section is in a transaction that has not been committed. When you make the REST call, the api/login controller will be run in a new transaction that cannot see your un-committed User.
A few options (there are others)...
Create User in BootStrap.groovy
def init = { servletContext ->
environments {
test {
new User(username: "securitySpecTestUserName", password: "securitySpecTestPassword").save(flush: true)
}
}
}
Make REST calls to create the User - assuming you have such functionality
Create User in setup
#Integration
#Rollback
class UserIntSpec extends Specification {
def setup() {
new User(username: "securitySpecTestUserName", password: "securitySpecTestPassword").save(flush: true)
}
void "check get access token"() {
given:
RestBuilder rest = new RestBuilder()
when:
def response = rest.post("http://localhost:${serverPort}/api/login") {
accept('application/json')
contentType('application/json')
json {
username = "securitySpecTestUserName"
password = "securitySpecTestPassword"
}
}
then:
response.status == HttpServletResponse.SC_OK
when:
def token = response.json.access_token
then:
token
}
}
Note: In Grails >= 3.0, setup() is run in a separate transaction and persisted (why it solves your problem) which is not rolled back. Any data will need to be cleaned up manually.
I suggest you read the grails documentation on testing: Integration Testing

Can't change session service name in torii

I'm using ember-simple-auth and emberfire to authenticate users on my app. One thing I don't like about the defaults is that there is both a "session" service and a "session" object on the service. So, I opened config/environment.js and changed:
var Env = {
torii: {
sessionServiceName: 'session',
providers: {
'firebase-simple-auth': {}
}
}
...
to
var Env = {
torii: {
sessionServiceName: 'auth',
providers: {
'firebase-simple-auth': {}
}
}
...
But, the newly named "auth" service doesn't have the "invalidate" and "authenticate" methods. Those are still on the "session" service (which I'm surprised is still around).
How do I move the entire "session" service over to an "auth" service?
Thanks!
You are configuring torii, not ESA. When you use the 2 in combination though you're not actually using torii's session at all. Ember Simple Auth's session service cannot be renamed but that's also not necessary anyway as you explicitly inject it anyway and can specify a custom name when doing so.

Grails Spring Security Rest - 403 Forbidden

So I just started learning Grails, and I am trying incorporate the Spring Security REST plugin into my app, the plugin is installed in addition to spring security core which is working. In my REST client, when I hit "api/login" I am able to get an access token and it says I have the role of "ROLE_ADMIN", but then when I try to hit something using that, I keep getting a 403 Forbidden. In Postman, the REST client I am using, I have my Authorization header with "Bearer {key}", with my url of "http://localhost:8080/test/api/secret" and it gives the 403 error. I am trying to setup the log4j logging to see any other issues, but does anyone know what I should look into, any help would be appreciated. I provided my classes below if that helps, I generally used default values for everything such as the UrlMappings.
RandomController.groovy
package test
import grails.plugin.springsecurity.annotation.Secured
#Secured(['IS_AUTHENTICATED_ANONYMOUSLY'])
class MyController {
#Secured(['ROLE_ADMIN'])
def secret() {
render "You have ACCESS!!!"
}
}
Bootstrap.groovy
class BootStrap {
def init = { servletContext ->
def adminRole = new SecRole(authority: 'ROLE_ADMIN').save(flush: true)
def testUser = new SecUser(username: 'bob', password: 'test')
testUser.save(flush: true)
SecUserSecRole.create testUser, adminRole, true
}
def destroy = {
}
}
UrlMappings.groovy
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/api/$controller/$action?/$id?(.$format)?"{ constraints { // apply constraints here
} }
"/"(view:"/index")
"500"(view:'/error')
}
}
For what I can see from the code you posted, if you invoke url http://localhost:8080/test/api/secret, it should execute default action (maybe index) in SecretController but the controller you posted is called MyController.
To investigate further, you should enable more verbose logging using log4j configuration as suggested in the doc http://alvarosanchez.github.io/grails-spring-security-rest/1.5.1/docs/guide/debugging.html

Resources