I'm building a custom Apache module (a hook, to be specific) that checks for a custom authorization cookie provided by an external service, and then either validates and decrypts the cookie to get user information, or if not valid redirects the user to our custom login service. The main function is pretty straightforward, looks something like this:
static int verifyRequest(request_rec* req) {
char* authCookie;
if (!req->main) {
authCookie = getAuthCookie(req);
if (isValidAuthCookie(authCookie, req)) {
/* do stuff, inject user data, etc... */
} else {
setLoginRedirect(req);
}
}
return OK;
}
So I want it to return OK if the user is authorized, I then inject user data (i.e. username) and do anything else, then I want the request to be processed by Apache as usual. However, if the user isn't valid I want to do a 302 redirect (by the way, if there's a better way to redirect the user from an Apache module let me know), as the setLoginRedirect function inserts our Login service's URL into the 'Location' field on the response header. If I try to return 302 from the else block, I get an empty response. If I declare an int at the top of the function and set it to OK (or 0) if valid and then set it to HTTP_TEMPORARY_REDIRECT (or 302) from the else block and then return the int at the bottom, I get an empty response. I've also tried using boolean values with an if statement at the bottom, and I've also tried doing the same thing by checking the header for the Location value at the bottom - both result in an empty response.
Any idea how to change the return value based on the if statement in the middle?
Related
I am following the Implicit Workflow example from the angular-oauth2-oidc documentation.
Everything works well in my Angular app, and I can login (during which I am redirected to Identity Server), get my token and use this token to access my Web Api.
However, I have noticed that the "given_name" claim is null, and therefore, the username is not displayed on the login page. Specifically, the following method from the sample code appears to return null:
public get name() {
let claims = this.oauthService.getIdentityClaims();
if (!claims) return null;
return claims.given_name;
}
I thought perhaps this was a problem with permissions, but my scope is set to:
scope: 'openid profile email api1',
Any idea what I need to change to get this "given_name" claim?
For those who encountered the same issue. You can fix it by adding this line AlwaysIncludeuserClaimsInIdToken=true in the client settings on identity provider side.
OauthService.getIdentityClaims() is a Promise and holds UserInfo you can extract the name field with braces, so your function should be:
public get name() {
let claims = this.oauthService.getIdentityClaims();
if (!claims) return null;
return claims['name'];
}
The answer marked as "Best answer" is not correct. Get the user claims in the 'idtoken' will cause that the 'idtoken' be very big and then you may exceed the size limit.
The correct implementation is to use the 'UserInfo' Endpoint and then use the method 'loadUserProfile':
Example:
getUserClaims() {
const user = this.oauthService.loadUserProfile();
console.log(user, user);
}
I had the same issue, in my case with an error displayed on the browser console, saying that Request was blocked by Security Policy.
even having the AllowAnyOrigin() method called in startup, I lacked to get the header allowed. So when in my angular aap i call the loadUserProfile method via the
token_received event, it sends some headers that were not allowed.
Finaly this fix my issue:
app.UseCors(options => options.AllowAnyOrigin().AllowAnyHeader());
Don't forget calling that before usemvc
I am trying to use Spring Social on my application and I noticed while debugging that the original 'OAuth2' state parameter is always null on my app.
See Spring Social source code for org.springframework.social.connect.web.ConnectSupport below:
private void verifyStateParameter(NativeWebRequest request) {
String state = request.getParameter("state");
String originalState = extractCachedOAuth2State(request);//Always null...
if (state == null || !state.equals(originalState)) {
throw new IllegalStateException("The OAuth2 'state' parameter is missing or doesn't match.");
}
}
private String extractCachedOAuth2State(WebRequest request) {
String state = (String) sessionStrategy.getAttribute(request, OAUTH2_STATE_ATTRIBUTE);
sessionStrategy.removeAttribute(request, OAUTH2_STATE_ATTRIBUTE);
return state;
}
Can anyone please help?
edit: I do see the state parameter being passed back by facebook:
Request URL:https://www.facebook.com/v2.5/dialog/oauth?client_id=414113641982912&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fconnect%2Ffacebook&scope=public_profile&state=0b7a97b5-b8d1-4f97-9b60-e3242c9c7eb9
Request Method:GET
Status Code:302
Remote Address:179.60.192.36:443
edit 2: By the way, the exception I get is the following:
Exception while handling OAuth2 callback (The OAuth2 'state' parameter is missing or doesn't match.). Redirecting to facebook connection status page.
It turned out that the issue was caused by the fact that I was relying on headers - as opposed to cookies - to manage the session.
By commenting out the following spring session configuration bean:
#Bean
public HttpSessionStrategy sessionStrategy(){
return new HeaderHttpSessionStrategy();
}
The oauth2 state parameter issue was sorted.
P.S. Now I have got to find a way to get Spring Social to work with my current configuration of Spring Session...
Edit: I managed to keep the HeaderHttpSessionStrategy (on the spring session side) and get it to work by implementing my own SessionStrategy (on the spring social side) as follows:
public class CustomSessionStrategy implements SessionStrategy {
public void setAttribute(RequestAttributes request, String name, Object value) {
request.setAttribute(name, value, RequestAttributes.SCOPE_SESSION);
}
public Object getAttribute(RequestAttributes request, String name) {
ServletWebRequest servletWebRequest = (ServletWebRequest) request;
return servletWebRequest.getParameter(name);
}
public void removeAttribute(RequestAttributes request, String name) {
request.removeAttribute(name, RequestAttributes.SCOPE_SESSION);
}
}
Try this work around and see if that works for you:
To my surprise I opened application in a 'incognito' browser and everything worked. Just like that. I think before something got cached and was causing the issue.
I ran into this issue today, My application was working perfectly fine. I just took a break for few hours and when I ran it again it started complaining about 'The OAuth2 'state' parameter is missing or doesn't match.'
The state param is first put into the session then the request goes out to facebook and the request comes back with the same state param but when spring is looking for session object to get the state param, it is not finding the session. I think it is not finding the session because when the request comes back it thinks that it is a different client (or host), even though the old HttpSession object still exists. The container maintains a HttpSession per client.
What you're getting from Facebook is not a request attribute , it's a request parameter.
You should get it by something like:
request.getParameter("state")
I have two separate projects: UI(AngularJS) and Server(Symfony2).
I need to send cross-domain PUT request from AngularJS application to the Symfony2 app.
In the Symfony controller I passed $request to the form->handleRequest(); and debug showed me, that form using this way is not submitted.
So, next I tried to pass $request to the form->submit() and got error "Invalid CSRF Token".
How can I correctly process cross-domain data via Symfony forms?
I've read that passing $request to the submit() method is
depricated.
How can I pass CSRF token to the form if I send it from UI via
headers ? (I add csrf field to the request but it not processing at back-end)
EDITED: Currently I see that issue is related to CSRF token. No matter how I sending this token from UI, it's not processed on back-end and I always get "Invalid CSRF token" error.
I tried to add _token field directly to json object and set field name to _token via csrf_field_name option into my formtype class.
I also tried to pass json_decode($request->getContent(), true) to my form submit() method, but after debugging I see, that submittedData is changing in next code :
// Symfony/Component/Form/Form.php, submit() method
if ($dispatcher->hasListeners(FormEvents::PRE_SUBMIT)) {
// here submittedData has csrf _token key/value
$event = new FormEvent($this, $submittedData);
$dispatcher->dispatch(FormEvents::PRE_SUBMIT, $event);
$submittedData = $event->getData();
// now submittedData without _token key/value
}
EDITED2: more details. CsrfValidationListener that using by Symfony Form component call $this->tokenManager->isTokenValid(new CsrfToken($this->tokenId, $data[$this->fieldName])) and this return false, the issue in next code:
// Symfony/Component/Security/Csrf/CsrfTokenManager.php
public function isTokenValid(CsrfToken $token)
{
if (!$this->storage->hasToken($token->getId())) {
return false;
}
return StringUtils::equals($this->storage->getToken($token->getId()), $token->getValue());
}
It seems csrf token is stored into session, so isTokenValid() method return false.
I continue to debug.
EDITED3:
as I can see, session is empty on calling $this->storage->hasToken($token->getId()) from CsrfTokenManager.php.
This is very strange, because I generate csrf token from my controller in next way:
$csrfToken = $this->get('security.csrf.token_manager')->refreshToken('Symfony');
And as I can see, refreshToken() method save csrf token into db:
// Csrf/CsrfTokenManager.php
public function refreshToken($tokenId)
{
$value = $this->generator->generateToken();
$this->storage->setToken($tokenId, $value);
return new CsrfToken($tokenId, $value);
}
// Csrf/TokenStorage/SessionTokenStorage.php
public function setToken($tokenId, $token)
{
if (!$this->session->isStarted()) {
$this->session->start();
}
$this->session->set($this->namespace.'/'.$tokenId, (string) $token);
}
But when I send data to the form, $this->tokenManager->isTokenValid(new CsrfToken($this->tokenId, $data[$this->fieldName])) that calls from preSubmit() method of CsrfValidationListener return empty session.
just in case I add my security.yml settings, maybe I have missed something:
main:
pattern: ^/(?!login).+
stateless: true
simple_preauth:
authenticator: app_bundle.api_key_authenticator
provider: api_key_user_provider
anonymous: ~
logout: ~
login:
pattern: ^/login
stateless: false
simple_preauth:
authenticator: app_bundle.email_password_authenticator
provider: email_user_provider
anonymous: ~
Notice: I generate csrf-token under login firewall and try to access it from main firewall!
But I also tried to generate csrf-token in the same firewall. Nothing changed.
EDITED4:
I have configured custom session dir for tracking session creation. So, I can see, that on login I have session with all attributes, but when I doing PUT request, I notice that new session file is created and it contains something like this:
_sf2_attributes|a:0:{}_sf2_flashes|a:0:{}_sf2_meta|a:3:{s:1:"u";i:1449700968;s:1:"c";i:1449700968;s:1:"l";s:1:"0";}
Just empty session.
So, I have found the reason of csrf error behavior.
When csrf token created, it stored into session. Because of my api firewall is stateless, it can't store csrf token. And also on each authentication session is drop and has only current authentication token.
Properly configured CORS help to protect from csrf attack.
See also this answer about CORS headers.
Hope, this will be helpful for somebody.
I'm building a closed app (users need to authenticate in order to use it). I'm having trouble in identifying the currently authenticated user from my Latchet session. Since apache does not support long-lived connections, I host Latchet on a separate server instance. This means that my users receive two session_id's. One for each connection. I want to be able to identify the current user for both connections.
My client code is a SPA based on AngularJS. For client WS, I'm using the Autobahn.ws WAMP v1 implementation. The ab framework specifies methods for authentication: http://autobahn.ws/js/reference_wampv1.html#session-authentication, but how exactly do I go about doing this?
Do I save the username and password on the client and retransmit these once login is performed (which by the way is separate from the rest of my SPA)? If so, won't this be a security concearn?
And what will receive the auth request server side? I cannot find any examples of this...
Please help?
P.S. I do not have reputation enough to create the tag "Latchet", so I'm using Ratchet (which Latchet is built on) instead.
Create an angularjs service called AuthenticationService, inject where needed and call it with:
AuthenticationService.check('login_name', 'password');
This code exists in a file called authentication.js. It assumes that autobahn is already included. I did have to edit this code heavily removing all the extra crap I had in it,it may have a syntax error or two, but the idea is there.
angular.module(
'top.authentication',
['top']
)
.factory('AuthenticationService', [ '$rootScope', function($rootScope) {
return {
check: function(aname, apwd) {
console.log("here in the check function");
$rootScope.loginInfo = { channel: aname, secret: apwd };
var wsuri = 'wss://' + '192.168.1.11' + ':9000/';
$rootScope.loginInfo.wsuri = wsuri;
ab.connect(wsuri,
function(session) {
$rootScope.loginInfo.session = session;
console.log("connected to " + wsuri);
onConnect(session);
},
function(code,reason) {
$rootScope.loginInfo.session = null;
if ( code == ab.CONNECTION_UNSUPPORTED) {
console.log(reason);
} else {
console.log('failed');
$rootScope.isLoggedIn = 'false';
}
}
);
function onConnect(sess) {
console.log('onConnect');
var wi = $rootScope.loginInfo;
sess.authreq(wi.channel).then(
function(challenge) {
console.log("onConnect().then()");
var secret = ab.deriveKey(wi.secret,JSON.parse(challenge).authextra);
var signature = sess.authsign(challenge, secret);
sess.auth(signature).then(onAuth, ab.log);
},ab.log
);
}
function onAuth(permission) {
$rootScope.isLoggedIn = 'true';
console.log("authentication complete");
// do whatever you need when you are logged in..
}
}
};
}])
then you need code (as you point out) on the server side. I assume your server side web socket is php coding. I can't help with that, haven't coded in php for over a year. In my case, I use python, I include the autobahn gear, then subclass WampCraServerProtocol, and replace a few of the methods (onSessionOpen, getAuthPermissions, getAuthSecret, onAuthenticated and onClose) As you can envision, these are the 'other side' of the angular code knocking at the door. I don't think autobahn supports php, so, you will have to program the server side of the authentication yourself.
Anyway, my backend works much more like what #oberstat describes. I establish authentication via old school https, create a session cookie, then do an ajax requesting a 'ticket' (which is a temporary name/password which i associate with the web authenticated session). It is a one use name/password and must be used in a few seconds or it disappears. The point being I don't have to keep the user's credentials around, i already have the cookie/session which i can create tickets that can be used. this has a neat side affect as well, my ajax session becomes related to my web socket session, a query on either is attributed to the same session in the backend.
-g
I can give you a couple of hints regarding WAMP-CRA, which is the authentication mechnism this is referring:
WAMP-CRA does not send passwords over the wire. It works by a challenge-response scheme. The client and server have a shared secret. To authenticate a client, the server will send a challenge (something random) that the client needs to sign - using the secret. And only the signature is sent back. The client might store the secret in browser local storage. It's never sent.
In a variant of above, the signing of the challenge the server sends is not directly signed within the client, but the client might let the signature be created from an Ajax request. This is useful when the client was authenticated using other means already (e.g. classical cookie based), and the signing can then be done in the classical web app that was authenticating.
Ok, Greg was kind enough to provide a full example of the client implementation on this, so I wont do anything more on that. It works with just a few tweaks and modifications to almost any use-case I can think of. I will mark his answer as the correct one. But his input only covered the theory of the backend implementation, so I will try to fill in the blanks here for postparity.
I have to point out though, that the solution here is not complete as it does not give me a shared session between my SPA/REST connection and my WS connection.
I discovered that the authentication request transmitted by autobahn is in fact a variant of RPC and for some reason has hardcoded topic names curiously resembling regular url's:
- 'http://api.wamp.ws/procedure#authreq' - for auth requests
- 'http://api.wamp.ws/procedure#auth' - for signed auth client responses
I needed to create two more routes in my Laravel routes.php
// WS CRA routes
Latchet::topic('http://api.wamp.ws/procedure#authreq', 'app\\socket\\AuthReqController');
Latchet::topic('http://api.wamp.ws/procedure#auth', 'app\\socket\\AuthReqController');
Now a Latchet controller has 4 methods: subscribe, publish, call and unsubscribe. Since both the authreq and the auth calls made by autobahn are RPC calls, they are handled by the call method on the controller.
The solution first proposed by oberstet and then backed up by Greg, describes a temporary auth key and secret being generated upon request and held temporarily just long enough to be validated by the WS CRA procedure. I've therefore created a REST endpoint which generates a persisted key value pair. The endpoint is not included here, as I am sure that this is trivial.
class AuthReqController extends BaseTopic {
public function subscribe ($connection, $topic) { }
public function publish ($connection, $topic, $message, array $exclude, array $eligible) { }
public function unsubscribe ($connection, $topic) { }
public function call ($connection, $id, $topic, array $params) {
switch ($topic) {
case 'http://api.wamp.ws/procedure#authreq':
return $this->getAuthenticationRequest($connection, $id, $topic, $params);
case 'http://api.wamp.ws/procedure#auth':
return $this->processAuthSignature($connection, $id, $topic, $params);
}
}
/**
* Process the authentication request
*/
private function getAuthenticationRequest ($connection, $id, $topic, $params) {
$auth_key = $params[0]; // A generated temporary auth key
$tmpUser = $this->getTempUser($auth_key); // Get the key value pair as persisted from the temporary store.
if ($tmpUser) {
$info = [
'authkey' => $tmpUser->username,
'secret' => $tmpUser->secret,
'timestamp' => time()
];
$connection->callResult($id, $info);
} else {
$connection->callError($id, $topic, array('User not found'));
}
return true;
}
/**
* Process the final step in the authentication
*/
private function processAuthSignature ($connection, $id, $topic, $params) {
// This should do something smart to validate this response.
// The session should be ours right now. So store the Auth::user()
$connection->user = Auth::user(); // A null object is stored.
$connection->callResult($id, array('msg' => 'connected'));
}
private function getTempUser($auth_key) {
return TempAuth::findOrFail($auth_key);
}
}
Now somewhere in here I've gone wrong. Cause if I were supposed to inherit the ajax session my app holds, I would be able to call Auth::user() from any of my other WS Latchet based controllers and automatically be presented with the currently logged in user. But this is not the case. So if somebody see what I'm doing wrong, give me a shout. Please!
Since I'm unable to get the shared session, I'm currently cheating by transmitting the real username as a RPC call instead of performing a full CRA.
Anybody have an idea, why cookie in Cakephp always get deleted automatically after function ends?
I try to write a cookie in let say function abc, with following :
$this->Cookie->write('referal', $ref);
Before that, in app controller before filter, i have initialized the cookie as following :
$this->Cookie->name = 'renttycoons';
$this->Cookie->time = 604800; // or '1 week'
$this->Cookie->path = '/';
$this->Cookie->domain = 'rent.local';
$this->Cookie->key = 'qSI232qs*&sXOw!';
But once the execution of function ends, the cookie was empty. when i try to read the cookie before function abc ends, it was there. There was no delete cookie method anyway.
yes, because the way Cookie in Cake works: when you use Cookie->write(), it doesn't directly write to the cookie, because the cookie is in the user's browser. Only until the view is rendered that the cookie you wrote is sent. So when you redirect, (I would guess the cookie doesn't get sent and flushed out because the view isn't rendered) the new request has the old cookie data.
If you want to persist some shared data within cake app, and unique to each visitor, use SessionComponent. It looks pretty much the same as Cookie: $this->Session->write('referal', $ref); and $this->Session->read('referal');