Https redirect and login cookies on Heroku with Play Framework - https

I have a Play! framework Heroku project that has three deployments. One for running my dev machine, one for beta on Heroku, and one for production on Heroku. Their http and https urls are as follows:
DEV BETA PRODUCTION
HTTP URL | http://localhost:9000 http://domain-beta.herokuapps.com http://www.domain.com
HTTPS URL | https://localhost:9443 https://domain-beta.herokuapps.com https://secure.domain.com
HTTPS Type | My cert Piggyback (using Heroku's cert) Hostname-based SSL (using my cert)
I also have a class HttpsRequired that has methods for requiring HTTPS, and for redirecting back to HTTP (thanks to this post for the help).
public class HttpsRequired extends Controller {
/** Called before every request to ensure that HTTPS is used. */
#Before
public static void redirectToHttps() {
//if it's not secure, but Heroku has already done the SSL processing then it might actually be secure after all
if (!request.secure && request.headers.get("x-forwarded-proto") != null) {
request.secure = request.headers.get("x-forwarded-proto").values.contains("https");
}
//redirect if it's not secure
if (!request.secure) {
String url = redirectHostHttps() + request.url;
System.out.println("Redirecting to secure: " + url);
redirect(url);
}
}
/** Renames the host to be https://, handles both Heroku and local testing. */
#Util
public static String redirectHostHttps() {
if (Play.id.equals("dev")) {
String[] pieces = request.host.split(":");
String httpsPort = (String) Play.configuration.get("https.port");
return "https://" + pieces[0] + ":" + httpsPort;
} else {
if (request.host.endsWith("domain.com")) {
return "https://secure.domain.com";
} else {
return "https://" + request.host;
}
}
}
/** Renames the host to be https://, handles both Heroku and local testing. */
#Util
public static String redirectHostNotHttps() {
if (Play.id.equals("dev")) {
String[] pieces = request.host.split(":");
String httpPort = (String) Play.configuration.get("http.port");
return "http://" + pieces[0] + ":" + httpPort;
} else {
if (request.host.endsWith("domain.com")) {
return "http://www.domain.com";
} else {
return "http://" + request.host;
}
}
}
}
I modified Secure.login() to call HttpsRequired.redirectToHttps() before it runs, to ensure that all passwords are submitted encrypted. Then, in my Security.onAuthenticated(), I redirect to the homepage on standard HTTP.
This works great on my dev and beta deployments, but in production all of my HTTP requests are redirected to the HTTPS login page. I can still use the whole site in HTTPS, but I want regular HTTP to work too.
All of my pages are protected as members-only and require users to login, using the #With(Secure.class) annotation. I'm thinking that it must be related to the fact that the login happens at secure.domain.com instead of www.domain.com, and that they somehow generate different cookies.
Is there a way to change the login cookie created at secure.domain.com to make it work at www.domain.com?

Check out the documentation for the setting for default cookie domain.
http://www.playframework.org/documentation/1.2.4/configuration#application.defaultCookieDomain
It explains how you can set a cookie to work across all subdomains.
application.defaultCookieDomain
Enables session/cookie sharing between subdomains. For example, to
make cookies valid for all domains ending with ‘.example.com’, e.g.
foo.example.com and bar.example.com:
application.defaultCookieDomain=.example.com

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.

Vapor 3 Websocket with Sessions

In Vapor 2 it was possible to access a session when connecting a new websocket.
For example:
setupRoutes(){
socket("ws") { request, websocket in
let session = try request.assertSession()
guard let userId = session.data["user_id"]?.string else {
..
}
}
In Vapor 3 configure.swift:
let wss = NIOWebSocketServer.default()
wss.get("ws") { websocket, request in
--get session information--
websocket.onText { websocket, text in
websocket.send(text)
}
}
services.register(wss, as: WebSocketServer.self)
With Vapor 3 the SessionMiddleware will not be invoked before passing the HTTP upgrade request to the WebsocketServer.
So how can I access session information?
So, I'm super aware that this thread is old and the OP probably found an answer or gave up months ago. Just in case anyone comes across this still looking, can't you use websocket.session to access the session?
This would make the Vapor 3 code
let wss = NIOWebSocketServer.default()
wss.get("ws") { websocket, request in
guard let userID = (try? websocket.session).data["user_id"]?.string else {
...
}
websocket.onText { websocket, text in
websocket.send(text)
}
}
services.register(wss, as: WebSocketServer.self)

Web API add openid scope to auth url for swagger/swashbuckle UI

We have a asp.net web api application which uses swagger/swashbuckle for it's api documentation. The api is secured by azure AD using oauth/openid-connect. The configuration for swagger is done in code:
var oauthParams = new Dictionary<string, string>
{
{ "resource", "https://blahblahblah/someId" }
};
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.SingleApiVersion(Version, Name);
c.UseFullTypeNameInSchemaIds();
c.OAuth2("oauth2")
.Description("OAuth2 Implicit Grant")
.Flow("implicit")
.AuthorizationUrl(
"https://login.microsoftonline.com/te/ourtenant/ourcustompolicy/oauth2/authorize")
.TokenUrl(
"https://login.microsoftonline.com/te/ourtenant/ourcustompolicy/oauth2/token");
c.OperationFilter<AssignOAuth2SecurityRequirements>();
})
.EnableSwaggerUi(c =>
{
c.EnableOAuth2Support(_applicationId, null, "http://localhost:49919/swagger/ui/o2c-html", "Swagger", " ", oauthParams);
c.BooleanValues(new[] { "0", "1" });
c.DisableValidator();
c.DocExpansion(DocExpansion.List);
});
When swashbuckle constructs the auth url for login, it automatically adds:
&scope=
However I need this to be:
&scope=openid
I have tried adding this:
var oauthParams = new Dictionary<string, string>
{
{ "resource", "https://blahblahblah/someId" },
{ "scope", "openid" }
};
But this then adds:
&scope=&someotherparam=someothervalue&scope=openid
Any ideas how to add
&scope=openid
To the auth url that swashbuckle constructs?
Many thanks
So, found out what the issue was, the offending code can be found here:
https://github.com/swagger-api/swagger-ui/blob/2.x/dist/lib/swagger-oauth.js
These js files are from a git submodule that references the old version of the UI.
I can see on lines 154-158 we have this code:
url += '&redirect_uri=' + encodeURIComponent(redirectUrl);
url += '&realm=' + encodeURIComponent(realm);
url += '&client_id=' + encodeURIComponent(clientId);
url += '&scope=' + encodeURIComponent(scopes.join(scopeSeparator));
url += '&state=' + encodeURIComponent(state);
It basically adds scopes regardless of whether there are scopes or not. This means you cannot add scopes in the additionalQueryParams dictionary that gets sent into EnableOAuth2Support as you will get a url that contains 2 scope query params i.e.
&scope=&otherparam=otherparamvalue&scope=openid
A simple length check around the scopes would fix it.
I ended up removing swashbuckle from the web api project and added a different nuget package called swagger-net, found here:
https://www.nuget.org/packages/Swagger-Net/
This is actively maintained and it resolved the issue and uses a newer version of the swagger ui. The configuration remained exactly the same, the only thing you need to change is your reply url which is now:
http://your-url/swagger/ui/oauth2-redirect-html

Grails session attributes are not flushed between two applications

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
}
}
}
}
}

How do you restrict access to certain paths using Lighttpd?

I would like to restrict access to my /admin URL to internal IP addresses only. Anyone on the open Internet should not be able to login to my web site. Since I'm using Lighttpd my first thought was to use mod_rewrite to redirect any outside request for the /admin URL back to my home page, but I don't know much about Lighty and the docs don't say much about detecting a 192.168.0.0 IP range.
Try this:
$HTTP["remoteip"] == "192.168.0.0/16" {
/* your rules here */
}
Example from the docs:
# deny the access to www.example.org to all user which
# are not in the 10.0.0.0/8 network
$HTTP["host"] == "www.example.org" {
$HTTP["remoteip"] != "10.0.0.0/8" {
url.access-deny = ( "" )
}
}
This worked for me:
$HTTP["remoteip"] != "192.168.1.1/254" {
$HTTP["url"] =~ "^/intranet/" {
url.access-deny = ( "" )
}
}
!= worked over ==.

Resources