Spring-security-core:3.0.3. Ajax. How to disable redirection to page after /login/authenticate? - grails-3.0

How to disable redirection to page after /login/authenticate?
And get a response { "success": true, "message": "Welcome"}, without redirecting to page.
For failing to get { "success": false, "message": "Sorry, we were not able to find a user with that username and password"}, without redirecting to page.
At the established parameters:
grails.plugin.springsecurity.useBasicAuth = true
grails.plugin.springsecurity.securityConfigType = 'InterceptUrlMap'
grails.plugin.springsecurity.filterChain.chainMap = [
[pattern:'/**/js/**', filters: 'JOINED_FILTERS,-exceptionTranslationFilter'],
[pattern:'/**/css/**', filters: 'JOINED_FILTERS,-exceptionTranslationFilter'],
[pattern:'/**/images/**', filters: 'JOINED_FILTERS,-exceptionTranslationFilter'],
[pattern:'/assets/**', filters: 'JOINED_FILTERS,-exceptionTranslationFilter'],
[pattern:'/account/**', filters: 'JOINED_FILTERS,-exceptionTranslationFilter'],
[pattern:'/api/**', filters: 'JOINED_FILTERS,-basicAuthenticationFilter,-basicExceptionTranslationFilter'] ]
Not propose Spring Security REST plugin.

You have implemented your ajax login like this?
http://grails-plugins.github.io/grails-spring-security-core/v3/index.html#ajax
Default behaviour should be almost as you described (there's no success:false when failing, only error).
When successfully authenticated, ajaxSuccess -method from LoginController is called. When failing authfail or ajaxDenied -is called. And it only renders JSON.
https://github.com/grails-plugins/grails-spring-security-core/blob/master/grails-app/controllers/grails/plugin/springsecurity/LoginController.groovy

Related

Identity Server 4 Silent Renew ErrorResponse: login_required

I have cloned the repo from the redux-oidc-example and it works for the most part but after a few hours it gives the following error:
Action payload: ErrorResponse: login_required
at new e (oidc-client.min.js:1)
at t [as _processSigninParams] (oidc-client.min.js:1)
at t [as validateSigninResponse] (oidc-client.min.js:1)
at oidc-client.min.js:1
UserManager.js looks like this:
const userManagerConfig = {
client_id: 'js.dev',
client_secret: 'secret',
redirect_uri: `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/callback`,
response_type: 'id_token token',
scope: 'openid email profile role offline_access',
authority: 'http://localhost:8080',
silent_redirect_uri: `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/silent_renew.html`,
automaticSilentRenew: true,
filterProtocolClaims: true,
loadUserInfo: true
};
and my identity server config:
{
"Enabled": true,
"ClientId": "js.dev",
"ClientName": "Javascript Client",
"ClientSecrets": [ { "Value": "K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols=" } ],
"AllowedGrantTypes": [ "implicit", "authorization_code" ],
"AllowedScopes": [ "openid", "email", "profile", "role", "offline_access" ],
"AllowOfflineAccess": true,
"AllowAccessTokensViaBrowser":true,
"RedirectUris": [
"http://localhost:8081/callback",
"http://localhost:8081/silent_renew.html"
],
"PostLogoutRedirectUris": [
"http://localhost:8081"
],
"AccessTokenLifetime": 900,
"RequireConsent": false
}
I noticed that prior to error last valid response had one cookie response(idsrv.session) with empty value with the expiry date set to the previous year:
I believe this to be the root cause of the issue, I searched it on related Github repo and tried to add the Cookie.SameSite to none but it didn't help:
services.AddAuthentication()
.AddSaml(Configuration,externalProviders.UseSaml)
.AddCookie(options => {
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.Cookie.SameSite = SameSiteMode.None;
});
Any idea!
This is likely due to your IDP session expiring - if you call the authorize endpoint with prompt=none but it's unable to satisfy that request because no valid session exists (i.e. authentication cookie does not exist or has expired) then it will return error=login_required.
If this occurs then the correct course of action is to do an interactive (i.e. prompt=login) sign in request in the top level browser window.
After searching the Identity Server 4 repo, I made the following changes to my code:
services.AddIdentityServer(options=>
{
options.Authentication.CookieLifetime = TimeSpan.FromDays(30);
options.Authentication.CookieSlidingExpiration = true;
})
.AddProfileService<ProfileService>()
.AddSigningCertificate(Configuration)
.AddInMemoryClients(Configuration.GetSection("IdentityServer:Clients"))
.AddInMemoryIdentityResources(Resources.GetIdentityResources());
It started working afterward, but you would have to login again after you close the browser or reopen a new tab I guess it's because of the sessionStorage.
When the session expires the signin-callback is being called by STS having a query parameter called 'error' with the value 'login_required'.
In the signin-callback, before completing sign-in, you can check for this query parameter and if it's found you can sign-out also from your web client.
I had the same issue and tried the proposed above, but for me, it actually was SameSiteMode not set correctly on IdentityServer Cookies. It caused Callback error: ErrorResponse: login_required right after login and after N attempts user was logged out.
This helped me https://github.com/IdentityServer/IdentityServer4/blob/main/src/IdentityServer4/host/Extensions/SameSiteHandlingExtensions.cs
What they do is based on this article https://devblogs.microsoft.com/dotnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
Hope this is useful.
Update.
I had another issue related to this when the user was logged out after re-opening a browser (especially on Android Chrome). login_required error was shown. I noticed that session cookie Expires/Max-Age was set to Session and not some future date. Probably because of that check session iframe (with src={identity server url}/connect/checksession) failed as Identity Server thought there was no session as cookie expired.
I tried setting cookie lifetime via options, but it didn't work as expected for some reason. Lifetime was always 14 days:
services.AddIdentityServer(options=>
options.Authentication.CookieLifetime = TimeSpan.FromDays(30);
options.Authentication.CookieSlidingExpiration = true;
})
Then I tried this and it worked for me:
services.ConfigureApplicationCookie(options => {
options.ExpireTimeSpan = sessionCookieLifetime;
options.SlidingExpiration = true;
})

How to get authenticated to join private channel using Laravel Echo and Socket.io

I am using Laravel 5.8.10, React 16.8, Laravel Echo Server 1.5.2, Redis 3.2.9, and Socket.io 2.2.0.
I am NOT using Pusher and don't want to use Pusher.
I am trying to create a basic chat system for site users. They log in normally using session authentication with email and password - all of that works fine.
There are 2 types of users: Brands and Influencers. Each has its own custom guard (web-brands & web-influencers). All session guards work normally.
I'm building the chat page using React. I can successfully join a public channel and receive messages on that public channel. However, the problem is when I try to make the channel private.
When I try to join a private channel, Laravel Echo Server sends an authentication request to: http://localhost:8000/broadcasting/auth.
But that returns the following 401 error:
{"message":"Unauthenticated."}
Client can not be authenticated, got HTTP status 401
Right now, I am trying to authenticate requests to /broadcasting/auth using a simple 'api_token' that is stored in the users tables (brands and influencers are the 2 users tables). This is a unique 60-character string.
I am trying this 'api_token' strategy because it sounds easier than setting up Laravel Passport, but perhaps I am wrong about that.
This is the constructor method from my React page:
import React, { Component } from 'react';
import Echo from "laravel-echo";
import Socketio from "socket.io-client";
constructor(props) {
super(props);
this.state = {
currentConversationId: conversations[0].id,
data: '',
};
this.selectConversation = this.selectConversation.bind(this);
let echo = new Echo({
broadcaster: 'socket.io',
host: 'http://localhost:6001',
client: Socketio,
auth: {
headers: {
// I currently have CSRF requirements disabled for /broadcasting/auth,
// but this should work fine once it is enabled anyway
'X-CSRF-Token': document.head.querySelector('meta[name="csrf-token"]'),
// I have the api_token hard-coded as I am trying to get it to work,
// but I have also used the javascript variable 'token' below
'api_token':'uUOyxRgCkVLKvp7ICZ0gXaELBPPbWEL0tUqz2Dv4TsFFc7JO4gv5kUi3WL3Q',
'Authorization':'Bearer: ' +'uUOyxRgCkVLKvp7ICZ0gXaELBPPbWEL0tUqz2Dv4TsFFc7JO4gv5kUi3WL3Q',
//'api_token':token,
//'Authorization':'Bearer: ' + token,
}
}
});
// Note that the ID of 1 is hardcoded for now until I get it to work
echo.private('brand.1')
.listen('SimpleMessageEvent', event => {
console.log('got something...');
console.log(event);
this.state.data = event;
});
}
Here you can see the in $php artisan route:list, the route is using auth:api middleware:
| GET|POST|HEAD | broadcasting/auth | Illuminate\Broadcasting\BroadcastController#authenticate | auth:api
Here is my BroadcastServiceProvider.php:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Broadcast;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Broadcast::routes(['middleware' => ['auth:api']]);
require base_path('routes/channels.php');
}
}
Here is my auth.php:
<?php
return [
'defaults' => [
'guard' => 'web-brands',
'passwords' => 'brands',
],
'guards' => [
'web-brands' => [
'driver' => 'session',
'provider' => 'brands',
],
'web-influencers' => [
'driver' => 'session',
'provider' => 'influencers',
],
'api' => [
'driver' => 'token',
'provider' => 'brands2',
],
],
'providers' => [
'brands' => [
'driver' => 'eloquent',
'model' => App\Brand::class,
],
'influencers' => [
'driver' => 'eloquent',
'model' => App\Influencer::class,
],
'brands2' => [
'driver' => 'database',
'table' => 'brands',
],
],
'passwords' => [
'brands' => [
'provider' => 'brands',
'table' => 'password_resets',
'expire' => 60,
],
'influencers' => [
'provider' => 'influencers',
'table' => 'password_resets',
'expire' => 60,
],
],
];
Here is my channels.php:
Broadcast::channel('brand.{id}',true);
Note that I have the brand.{id} set it to return true by default. I have also tried this for channels.php:
Broadcast::channel('brand.{id}', function ($brand,$id) {
return $brand->id === Brand::find($id)->id;
});
I have already tried testing the simple api_token method by using a dummy route:
Route::get('test-test-test',function(){return 'asdf';})->middleware('auth:api');
This test works:
http://localhost:8000/test-test-test results in redirect
http://localhost:8000/test-test-test?api_token=123 results in redirect
http://localhost:8000/test-test-test?api_token=[the actual correct 60-character token] results in 'asdf'
Here is some info from my .env:
BROADCAST_DRIVER=redis
QUEUE_DRIVER=redis
CACHE_DRIVER=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
Here is my laravel-echo-server.json:
{
"authHost": "http://localhost:8000",
"authEndpoint": "/broadcasting/auth",
"clients": [],
"database": "redis",
"databaseConfig": {
"redis": {},
"sqlite": {
"databasePath": "/database/laravel-echo-server.sqlite"
}
},
"devMode": true,
"host": null,
"port": "6001",
"protocol": "http",
"socketio": {},
"sslCertPath": "",
"sslKeyPath": "",
"sslCertChainPath": "",
"sslPassphrase": "",
"subscribers": {
"http": true,
"redis": true
},
"apiOriginAllow": {
"allowCors": false,
"allowOrigin": "",
"allowMethods": "",
"allowHeaders": ""
}
}
Perhaps I am not sending the api_token correctly in the header of the laravel echo request?
UPDATE/EDIT:
Now I have tried removing the auth:api middleware for the /broadcasting/auth route. I'm not sure if that was the correct thing to do.
That now produces a 403 error:
Client can not be authenticated, got HTTP status 403
UPDATE 2 - IMPORTANT
So I know this is not recommended, but I started changing some things inside of the laravel source files... I got it to work finally and now that I have figured it out, I would like to override the source files that I changed instead of actually changing them. I did save the originals so I can easily revert back.
One big challenge was that while changing the source files, I was not able to use the where() method, only the find() method to lookup users.
The key function that needed changing was retrieveUser() (which is located inside of Illuminate/Broadcasting/Broadcasters/Broadcaster.php.
The problem was that it kept trying to run:
return $request->user();
...but that user() function never worked, which is why it always returned a 403 forbidden error. I think it is because the actual Laravel Echo request was sent from React (in javascript frontend), so there was no user object attached to the request. In other words, it was like a guest making the request. That explains why the public channels worked, but the private ones didn't.
I never did figure out how to get the user information to be sent with the request through React, but I did figure out a workaround.
Basically what I had to do:
In my controller, encrypt the ID of the user and pass it to javascript as a variable.
Pass the encrypted ID variable through the Echo request as part of the header.
Modify the retrieveUser() function to use find(Crypt::decrypt($id)) to lookup the user instead of ->user() (or where() which was strangely not allowed).
From what I can tell, this seems like a decent strategy from a security perspective, but perhaps readers could point out if that is actually correct.
To hack your way into a private channel, you would have to know the ID of the channel you want to listen to, then pass it as an encrypted variable in the header of the request.
Maybe a potential hacker could say that he/she wants to listen to private channel 'brand.1' and all they would have to do is encrypt the number 1 and pass it through the header. I guess I don't know how that works enough to know whether that is possible.
Anyway my goals now are:
converting this into an override setup instead of explicitly changing the Laravel source code.
figuring out if passing the encrypted ID through the request header is secure enough for production.
It does seem like the encrypted ID in the header (which does change every time you run the request) is more secure than simply passing through an 'api_token' which would be a value stored in the users table and is what most people seem to do.

Issues using Azure Active Directory (ADAL) with Aurelia

I've added Azure Active Directory Library (ADAL) to an Aurelia CLI 0.31.3 project and I appear to be out of my element.
Here is my repository.
The issues that I'm running into are:
Accessing the "Reports" page for the first time allows you to log in.
When it returns from the Azure AD sign-in, the Aurelia app reloads
twice.
Clicking "Log In" when on the "Home" page allows you to log
in. When it returns from the Azure AD sign-in, the URL contains
"/token_id=XXXX" and the Aurelia router errors stating "Route not
found".
Some notes on the project:
There are only two views. "Home" does not require authentication. "Reports" does.
./src/app.ts , ./src/authorizeStep.ts , and ./src/sessionState.ts , should be the only places any authentication code exists.
Any insight as to how to resolve these would be greatly appreciated!
#juunas probably has a working solution, but my take on this would have been slightly different.
The error pretty much says it: "Route not found". Aurelia is trying to match "token_id" to a route while that value should probably be ignored after your AuthorizeStep used it.
It may suffice to simply add it to your home routes like so:
{ route: ['', 'home', 'token_id'], name: 'home', moduleId: 'resources/views/home/home', nav: true, title: "Home" },
If it still doesn't match, you could add a wildcard as well: token_id*
That will solve the router error. You haven't mentioned whether the authentication itself works or not - if the router error was the only problem, this should do the trick.
EDIT
To follow up on my comment, as an alternative to using a separate view/viewmodel as a route you can also do this:
config.map({
name: 'logInRedirectCallback',
navigationStrategy: (instruction: NavigationInstruction) => {
const token = instruction.getWildcardPath();
// [...] do magic / pass the token to wherever it's needed
instruction.router.navigateToRoute(""); // or any page that makes sense after logging in
},
route: 'token_id=*',
});
I have not tested this so you may need to tweak the exact place of that wildcard. This works under the assumption that token_id is matched as part of the path, not part of the query. But the general idea is to just intercept that token, process it however you need to and then go to some existing page.
I don't have much experience with Aurelia, but here are the steps I took to make it work at least partially.
Hopefully you can get it working fully :)
First, I enabled verbose logging for ADAL.JS in sessionState.ts:
constructor(aureliaConfiguration, sessionState) {
this.aureliaConfiguration = aureliaConfiguration;
if (!this.authContext) {
this.authContext = new AuthenticationContext(
{
cacheLocation: "localStorage",
clientId: this.aureliaConfiguration.get('azureADApplicationId'),
tenant: this.aureliaConfiguration.get('azureADTenant'),
postLogoutRedirectUri: window.location.origin
}
);
Logging = {
level: 3,
log: (msg) => console.log(msg)
}
}
this.appName = this.aureliaConfiguration.get('name');
this.appApiKey = this.aureliaConfiguration.get('api.key');
}
Then I found ADAL giving an error about invalid state.
I noticed the URL was not really what it should be.
Usually the Id token is in a fragment, e.g. #id_token=asdasdasdas....
Of course this is just the framework's feature of showing pretty client-side routes.
Turns out there is a way to tell Aurelia not to modify URLs with hashes, while still using the normal routing links:
configureRouter(config: RouterConfiguration, router: Router): void {
this.sessionState.router = router;
config.title = 'Aurelia';
config.options.root = '/';
config.options.pushState = true;
config.options.hashChange = false;
config.map([
{ route: ['', 'home'], name: 'home', moduleId: 'resources/views/home/home', nav: true, title: "Home" },
{ route: ['reports'], name: 'reports', moduleId: 'resources/views/reports/reports', nav: true, title: "Reports", settings: { auth: true } }
]);
config.addAuthorizeStep(AuthorizeStep);
}
I added one line there: config.options.hashChange = false;.
After doing that, ADAL logged a successful authentication.
But I did say the solution was partial.
The login button still says "Log in".
After a page refresh it does change to "Log out", but that is one thing that needs to be fixed.
Also, clicking on "reports" does cause a redirect to AAD, but it specifies the redirect URL as http://localhost:9000/reports.
The problem is that we would need to configure every single client-side route as a reply URL for this to work.
So instead of using the full URL, you will need to use just the domain in authorizeStep.ts:
if (!user) {
this.sessionState.authContext.config.redirectUri = window.location.origin;
this.sessionState.authContext.login();
}

Django REST Angular $http 403 Status

So I have a very simple API endpoint that is supposed to determine if a user is logged in. I did decide to make it a POST request for the sake of hiding the response from being accessed in the browser
class Check(APIView):
def post(self, request):
print request.user
if request.user.is_authenticated():
# a successful response for logged in
return Response(status = HTTP_200_OK)
# return an error status code for not logged in
return Response(status = HTTP_401_UNAUTHORIZED)
When logged out, I get AnonymousUser in the console and a 401 status code as expected. However when logged in as a superuser or any other user, I get no print output and a 403 status code. This indicates that for some reason the entire callback is never entered. I've been told that it is an issue of permissions but I have AllowAny enabled. Do you guys have any ideas?
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer'
],
'DEFAULT_PARSER_CLASSES': [
'rest_framework.parsers.JSONParser'
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication'
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
]
}

Firebase: how to require auth to list all nodes, yet allow anonymous read/write to individual nodes?

I'm writing an invitation application, and would like to email individual people unique URLs, e.g.
http://www.example.com/invitation.html?inviteID=-Jkbw6ycU7ZUOipmqlb5
The HTML app contains JavaScript that connects to a particular Firebase, looking up a node by the inviteID from the URL. Example:
https://my-firebase-123#firebaseio.com/-Jkbw6ycU7ZUOipmqlb5
Each top-level node looks roughly like
-Jkbw6ycU7ZUOipmqlb5: {
email: 'joe#gmail.com',
people: [
{name: 'Joe', accept: true},
{name: 'Jane', accept: false}
],
comments: 'Jane can't make it, but I'm looking forward to it!'
}
This already works great! But I'm having trouble understanding how to properly secure the data. I need the recipients to continue to be able to access those URLs without authentication - anyone who supplies a node ID can read and write to that node and its children - and yet I need to require auth to see the Firebase at its top level, so that invitees cannot see (or modify!) anyone else's responses without knowing other inviteIDs. How can I do this?
{
"rules": {
".read": ??
".write": ??
}
}
I expect both .read and .write will need a rule that means something like this:
"You requested a specific child node, not the top level node; otherwise you must be an authorized user (auth != null) to see the top level node."
The app is written in ReactJS and communicates with Firebase roughly like this:
componentWillMount: function() {
var dbAddress = 'my-firebase-123#firebaseio.com/';
this.firebaseRef = new Firebase(dbAddress + this.props.inviteId);
this.firebaseRef.on("value", function(dataSnapshot) {
this.setState(dataSnapshot.val());
}.bind(this));
},
onSend: function() {
this.firebaseRef.set(this.state);
},
I have been reading the various firebase docs trying to find a similar solution.
Assuming your firebase json structure is something like the following:
{ Invitations: {
-Jkbw6ycU7ZUOipmqlb5: {
email: 'joe#gmail.com',
people: [
{name: 'Joe', accept: true},
{name: 'Jane', accept: false}
],
comments: 'Jane can't make it, but I'm looking forward to it!'
}
-Jkbw6ycU7ZUOipmqlb6: {
... another invitation ...
}
-Jkbw6ycU7ZUOipmqlb7: {
... another invitation ...
}
}
I came up with the following security config which appears to do what you require:
{
"rules": {
".read": false,
".write": false,
"invitations": {
"$inviteid": {
".read": true,
".write": true
}
}
}
}
Actually the top level read/write false may be inferred because if I set the config as the following it seems to work in the same way:
{
"rules": {
"invitations": {
"$inviteid": {
".read": true,
".write": true
}
}
}
}
Now I cant seem to be able to browse the invitations as in if I try and mount at the following points I get permission denied (assuming your firebase address is https://my-firebase-123#firebaseio.com/:
this.firebaseRef = new Firebase('https://my-firebase-123#firebaseio.com/');
this.firebaseRef = new Firebase('https://my-firebase-123#firebaseio.com/invitations');
where as mounting at the following level lets me in:
this.firebaseRef = new Firebase('https://my-firebase-123#firebaseio.com/invitations/-Jkbw6ycU7ZUOipmqlb5');
Not sure if what I have done is actually achieving your requirements from a security perspective (i.e. is it actually secure?).
Would appreciate any feedback from the expert firebase community on this approach.

Resources