I've got a web application where I'm using the Rx.js for handling event streams. The app uses a rest api provided by a backend.
Mostly I make a subscription for an api call and when a request is done I render results and reset other controls states (hide progress elements and so on).
Some api calls can be failed when an auth token is expired and I have to make a user to login again (show a login popup or so).
I'm curious is there a way to "restore" an api call stream after a successful login? Where a user has not to provide additional actions to get a server response.
Primitive example of the current workflow:
var apiCallStream = $.ajaxAsObservable(params):
apiCallStream.subscribe(
result => renderResult(result),
err => handleError(err));
function handleError(err) {
if (err.xhr.error === 401) {
LoginPopup();
} else {
ErrorPopup(err);
}
}
Here some some (very rough pseudo-code), but you could do this with retryWhen:
// use Rx.DOM.get for an Observable Ajax GET
var source = Rx.DOM.get('/some/end/point')
.retryWhen(function(errors) {
// retryWhen: errors is a stream of errors
// whatever observable you return, when it emits, the
// observable you're operating on will be retried. (the entire thing)
return errors.filter(function(e) { return e.status === 401; })
// if status is 401, then tell the user to login
.flatMapLatest(function() { return doUserLogin; });
});
// a contrived observable that shows a form and
// return a stream of successful logins
var doUserLogin = Observable.create(function(observer) {
// show a form
var form = $('.my-form').show();
// when submit is fired on said form...
return Rx.Observable.fromEvent(form, 'submit')
// send a login over ajax
.flatMap(e => Rx.DOM.post('some/url/login', form.serialize()))
});
Hopefully that gives you something to start with.
Related
I am trying to integrate Keycloak login into my React app and I'm trying to get the JWT from keycloak. Here is the code:
const [keycloakState, setKeycloakState] = useState<any>();
const login = () => {
const keycloak = Keycloak("/keycloak.json");
keycloak.init({onLoad: 'login-required'}).then(authenticated => {
console.log('kk', keycloak)
console.log('at', authenticated)
setKeycloakState({ keycloak: keycloak, authenticated: authenticated });
}).catch(err => {
alert(err);
});
console.log('log after')
}
The login function is triggered when a button is clicked. It redirects properly to keycloak, I can log in and I am properly redirected to the app. The problem is that after the redirect back to the app with proper login the code in the then part of the chain is not executed, and even the 'log after' does not appear in the logs. The catch error part works fine.
Why might this be happening? I have keycloak-js added to my project.
I used to face this problem before. The way that I passed is separating the "init" function and then invoke it later.
Here is my example on jsFiddle: 'https://jsfiddle.net/gzq6j3yu/1/'
Our solution was to use the functions onAuthSuccess and onAuthError avaliable on the KeycloakInstance keycloak-js provides. (The documentation around this is a little shaky but you can find them if you check out the source code.) As the names imply these functions get called when an auth attempt is successful or unsuccessful, respectively.
note: in the following snippets this._instance replaces OP's keycloak constant.
Basic code snippet:
import Keycloak from 'keycloak-js';
...
// pulled from a class's init function from a custom Keycloak helper class so won't translate one for one but you get the point.
this._instance = Keycloak(configObject);
this._instance.onAuthSuccess = () => {
// code to execute on auth success
};
this._instance.onAuthError = () => {
// code to execute on auth error
};
this._instance.init(initOptions)
...
We also had a getter to get the token on the KeycloakInstance (or empty string) on the same class. This is an easy way to refer to the token in your code to check if it actually exists, etc. Here's what that'd look like inside the class.
get token() {
return this._instance ? this._instance.token : '';
}
Hope this can help out some folks.
I think the reason your fulfilled callback is not executed is the way your app interacts with Keycloak. You initialize the Keycloak-Adapter with onLoad: 'login-required' which will redirect the user to Keycloak - which means the Javascript execution is interrupted at this point. Keycloak will redirect the user back to your app and since you wrapped the Keycloak-Adapter in a function which is only executed when a button is clicked, the promise callback is not executed.
Simple example:
// do this on page load
keycloak.init({onLoad: 'login-required'}).then((authenticated) => {
console.log('authenticated', authenticated)
})
You will not see a "authenticated", false in your console when you open up your app. If the user is not authenticated, he will be redirected (so no chance to execute that callback). If he then comes back and is authenticated, the callback will be executed and authenticated should be true.
If you want the user to click a button, a setup like this should work:
// do this somewhere early in your App or main.js and in a way that this code is executed on page load
const keycloak = new Keycloak(configuration);
keycloak.init({onLoad: 'check-sso'}).then((authenticated) => {
if (authenticated) {
// do what needs to be done if sign in was successful, for example store an access token
} else {
// handle failed authentication
}
}).catch(err => {
alert(err);
});
const login = () => { // this could be an on-click event handler
keycloak.login();
};
check-sso won't redirect the user to Keycloak if unauthenticated, so the user can trigger the login when needed.
Keep in mind that your JavaScript code will run twice and you have to cover both cases, first the user is not authenticated and needs to be redirected to Keycloak and a second time once the user comes back from Keycloak (then we should get the information that the user is authenticated in .then().
I'm trying to finish the implementation of 3D Secure Authentication on my checkout process however since the 3DS Auth requires a modal pop-up using an iframe to redirect the user to the banks URL to approve the payment or not, since not all cards require 3DS Auth, my question is how can i trigger a modal pop-up from the back-end if the card needs to?
Example of the checkout
const checkout = async () => {
var status = await foo();
var next = await nextAction();
if(status === 'succeeded') {
// ...more code here...
newSale.save()
.then(() => {
//payment is processed
// ...code and redirect here...
})
.catch(err => console.log(err));
} else if(status === 'awaiting_next_action'){
console.log('3D Secure Authentication required');
console.log(next);
// I need to trigger the modal from the front-end here if needed...
} else if(paymentIntentStatus === 'awaiting_payment_method') {
console.log('The PaymentIntent encountered a processing error. ');
}
}
as shown above, that 3DS Auth modal only needs to pop up if needed and if not it must not show.
Then backend must pass certain parameter in the response, then you can use if, else condition to check if that condition exists or not then accordingly open the modal.
Our Redux application use JWT tokens for authentication. The access_token expires every 15 minutes and the refresh_token expires every 30 days. Both of them are provided by our API every time you log in and stored in the browser's local storage. If a secure endpoint receives a request with an expired token, it returns a 401 HTTP error.
Unfortunately, I don't know how to proceed to handle the refresh process without having a negative impact on the user. From a technical point of view, here is what I would like to achieve:
Action creator calls the API with an expired token
Client receives a 401 HTTP error
Client triggers a function that calls the API to obtain a new token (by providing the refresh token).
If the call fails (refresh_token is expired), prompt the user the re-enter its credentials to re-obtain both tokens then re-attempt the original request.
If the call succeeds, re-attempt the original request.
I would like to have a function that would handle the refreshing process and that would be called in the error handling portion of the action creator.
Here is what I have tried so far:
export function handleError(dispatch, current_func, error, handling) {
if(error.response) {
if(error.response.status === 401 && readToken("token") !== null) {
return attemptTokenRefresh(dispatch, current_func)
}
if(error.response.status === 422 && readToken("token") === null) {
return attemptTokenRefresh(dispatch, current_func)
}
}
return(handling())
}
export function attemptTokenRefresh(dispatch, on_success) {
let token = readToken("refresh_token");
let instance = axios.create({
headers: {"Authorization": token}
});
instance.post("api/refresh").then(response => {
if (response.data["token"]) {
storeToken("token", response.data["token"]);
on_success();
}
}).catch(error => {
//TODO: Allow user to sign back (prevent wiping the state)
});
}
dispatch refers to the dispatch function provided by Redux
current_func refers to the action creator
error refers to the error returned by the API
handling refers to the error handling function for other types of errors
Any help would be greatly appreciated :)
My use case is:
User requests asset from our API which fails because of JWT expiring (passed as an httpOnly cookie) - API returns a 401 status code.
We go and authenticate them (without the user doing anything) again using a refresh_token to retrieve a new JWT with a request from our client to auth0.
We send that new JWT to our API to be set as an httpOnly cookie to replace the expired one.
We then want to retry the original request the user made to the API in step 1.
I'm trying to use Observables within my Redux app with redux-observable. If you can think of another way of making the above user flow work I would be happy to hear how.
NB. Im using rxjs V5
export const fetchAssetListEpic = (action$, store) => {
return action$.ofType('FETCH_ASSET_LIST')
.switchMap( action => {
const options = {
crossDomain: true,
withCredentials: true,
url: uriGenerator('assetList', action.payload)
};
return ajax(options);
})
.map(fetchAssetListSuccess)
.retryWhen(handleError)
.catch(redirectToSignIn);
};
function handleError(err) {
return (err.status === 401) ?
/* Authenticate here [Step 2] */
/* Send new JWT to API [Step 3] */
/* If successful make original request again [Step 4] */
:
Observable.throw(err);
}
function redirectToSignIn() {
/*I will redirect here*/
}
So far I able to complete steps 1, 2 and 3 but not too sure of a way to add step 4. I may be completely off the mark but any help would be great!
Well one thing you probably won't want to do is allow the error to make it to the top level stream. Even if you do a catch you have effectively killed the top level stream. So unless your redirect is doing a hard redirect instead of a a soft one via something like react-router, you won't be able to use this epic any more.
Thus I would say that you want most of the logic to be encapsulated within the switchMap:
function withAuthorizedFlow(source) {
return source
.map(fetchAssetListSuccess)
// retryWhen takes a callback which accepts an Observable of errors
// emitting a next causes a retry, while an error or complete will
// stop retrying
.retryWhen(e => e.flatMap(err =>
Observable.if(
// Returns the first stream if true, second if false
() => err.status === 401,
reauthenticate, // A stream that will emit once authenticated
Observable.throw(err) // Rethrow the error
))
)
.catch(redirectToSignIn);
}
/** Within the epic **/
.switchMap(({payload}) => {
const options = {
crossDomain: true,
withCredentials: true,
url: uriGenerator('assetList', payload)
};
// Invoke the ajax request
return ajax(options)
// Attach a custom pipeline here
// Not strictly necessary but it keeps this method clean looking.
.let(withAuthorizedFlow);
})
The use of let above is completely optional, I threw it in to clean up the function. Essentially though you want to contain the error to the inner stream so that it can't halt the outer one. I am not sure which ajax library you are using but you should also confirm that it will in fact return a cold Observable otherwise you will need to wrap it in a defer block to in order for the retryWhen to work.
As part of a gnome-shell extension, I try to consume a webservice using xmlrpc. The webservice expects a basic authentication header. Using Soup, I got the following code (basically a blueprint from the great openweather extension):
function load_json_async() {
if (_httpSession === undefined) {
_httpSession = new Soup.Session();
} else {
// abort previous requests.
_httpSession.abort();
}
let message = Soup.xmlrpc_message_new (
"https://api.sipgate.net/RPC2",
"samurai.BalanceGet",
new GLib.Variant('()',1.0)
)
_httpSession.connect('authenticate',
Lang.bind(
this,function(session,message, auth,retryFlag){
auth.authenticate("xxxxx","xxxxx");
}
)
)
_httpSession.queue_message(
message,
Lang.bind(this,
function(_httpSession, message) {
try {
if (!message.response_body.data) {
log("hello1 "+message.response_body.status)
return;
} else {
log("got message-status:"+message.status_code)
}
log(message.response_body.data)
} catch (e) {
log("exception:"+e)
return;
}
return;
}));
return;
}
I am using Soup for building up the connection. The authenticate signal is executed before the queue-callback is executed.
Still, in the beginning within the callback, the response_body holded the status code 401 instead of the expected authorization. The given credentials where incorrect.After correcting this, the call went through. However, you always need two calls to the provider this way: first to get the information it uses BasicAuth, and the second to actually do the call.
Is there a way to give the authentication information directly with the first call?
It's possible to add the authorization directly into the request header
let auth = new Soup.AuthBasic()
auth.authenticate("xxx","xxx");
message.request_headers.append("Authorization",auth.get_authorization(message))
This prevents a first request without auth header and also allows use of REST services that don't return the correct status code on unauthorized requests and forward to a login page instead.