I have my api with dingo/laravel. Normally works without problems for mobile (android).
My AuthController#token in dingo/laravel:
public function tokenRefresh()
{
$token = JWTAuth::getToken(); // Header:Auth..Baerer ...
if (!$token) {
throw new BadRequestHttpException('Token not provided');
}
try {
$token = JWTAuth::refresh($token);
} catch (TokenInvalidException $e) {
throw new AccessDeniedHttpException('The token is invalid');
}
return $this->response->withArray(['token' => $token]);
}
i making another app with nw.js, and i use it requestify module.
My example login request:
requestify.request(this.authUrl, {
method : 'POST',
body : {
email: document.getElementById('email').value,
password: document.getElementById('password').value
},
headers : {
'X-Forwarded-By': 'me'
},
dataType: 'json'
}).then(function (response) {
var body = response.getBody();
alert(body.token);
});
its request normally return valid token. its ok.
How about expires token?
What should I do?
Maybe, kind of like ajaxSetup for all request before.
I need to automatic refresh token when token expires.
What do you recommend?
Related
I'm trying to create an SPA for a laravel REST API protected by Sanctum
Right now, on my SPA the user can log in without a problem and the API sends back the token; but then the SPA doesn't fetch the user. I mean, it doesn't even try to fetch it; no error, no request, no nothing. Login and logout work flawlessly, but I'm unable to fetch the user.
Here's my config for auth module ( v5 ):
auth: {
strategies: {
laravelSanctum: {
provider: 'laravel/sanctum',
url: 'https://XXXXXXXXXXXXX/api',
token: {
property: 'access_token',
required: true,
type: 'Bearer'
},
endpoints: {
login: { url: '/login', method: 'post' },
logout: { url: '/logout', method: 'post' },
user: { url: '/user', method: 'get' }
},
user: {
autoFetch: true
}
}
},
My login function. If I understand correctly, just after the login the laravel/sanctum provider should fetch the user data:
async login() {
try {
let response = await this.$auth.loginWith('laravelSanctum', { data: this.form })
console.log('response -> ', response)
this.$router.push('/')
} catch (error) {
console.log('Error Response login -> ', error.response)
}
},
My logout function, just for completion ( it shouldn't have anything to do with the problem ):
async logout() {
try {
let response = await this.$auth.logout()
console.log('responselogout -> ', response)
this.$router.push('/login')
} catch (error) {
console.log('Error Response -> ', error.response)
}
},
Out of despair, I even created a function to try to fetch the user manually 😅 :
async fetch() {
try {
let responseuser = await this.$auth.fetchUser()
console.log('responseuser -> ', responseuser)
let loggedin = await this.$auth.loggedIn
console.log('loggedin -> ', loggedin)
} catch (erroruser) {
console.log('Error Response user -> ', erroruser.response)
}
},
On login, everything's fine but there is no request to the user endpoint:
Login request
When I try to fetch it manually, there is no request either:
Undefined response
And then on logout, everything works as it should:
Logout request
If it made the request to the /user endpoint ( either automatically after login, or manually when I use the fetch function ) and the API rejected it, or if there was an empty answer ... I would have something to work with ( I'm in control of the API too ), but with no request I just don't know where to start debugging the problem.
Any tip would be useful. Thanks in advance!
Just passing by to say I could finally solve the problem! Yay! 🥳
There was no request to /user because there was no XSRF-TOKEN cookie. And there was no XSRF-TOKEN because of browser security.
Long story short, this solution worked for me -> https://github.com/nuxt-community/auth-module/issues/1164#issuecomment-839199946
I hope this is helpful for anyone on the same situation :)
I am using Vuejs SPA with Laravel API as backend. I successfully got the personal access token and store in localStorage and Vuex state like below.
token: localStorage.getItem('token') || '',
expiresAt: localStorage.getItem('expiresAt') || '',
I use the access token every time I send axios request to laravel api. Every thing works well. However, initially the token was set to 1 year expiration so when I develop I didn't care about token being expired and today suddenly I thought what is going to happen if token expired. So I set token expiry to 10 seconds in laravel AuthServiceProvier.php.
Passport::personalAccessTokensExpireIn(Carbon::now()->addSecond(10));
and then I logged in and after 10 seconds, every requests stopped working because the token was expired and got 401 unauthorised error.
In this case, how can I know if the token is expired? I would like to redirect the user to login page if token is expired when the user is using the website.
Be as user friendly as possible. Rather than waiting until the token expires, receiving a 401 error response, and then redirecting, set up a token verification check on the mounted hook of your main SPA instance and have it make a ajax call to e.g. /validatePersonalToken on the server, then do something like this in your routes or controller.
Route::get('/validatePersonalToken', function () {
return ['message' => 'is valid'];
})->middleware('auth:api');
This should return "error": "Unauthenticated" if the token is not valid. This way the user will be directed to authenticate before continuing to use the app and submitting data and then potentially losing work (like submitting a form) which is not very user friendly.
You could potentially do this on a component by component basis rather than the main instance by using a Vue Mixin. This would work better for very short lived tokens that might expire while the app is being used. Put the check in the mounted() hook of the mixin and then use that mixin in any component that makes api calls so that the check is run when that component is mounted. https://v2.vuejs.org/v2/guide/mixins.html
This is what I do. Axios will throw error if the response code is 4xx or 5xx, and then I add an if to check if response status is 401, then redirect to login page.
export default {
methods: {
loadData () {
axios
.request({
method: 'get',
url: 'https://mysite/api/route',
})
.then(response => {
// assign response.data to a variable
})
.catch(error => {
if (error.response.status === 401) {
this.$router.replace({name: 'login'})
}
})
}
}
}
But if you do it like this, you have to copy paste the catch on all axios call inside your programs.
The way I did it is to put the code above to a javascript files api.js, import the class to main.js, and assign it to Vue.prototype.$api
import api from './api'
Object.defineProperty(Vue.prototype, '$api', { value: api })
So that in my component, I just call the axios like this.
this.$api.GET(url, params)
.then(response => {
// do something
})
The error is handled on api.js.
This is my full api.js
import Vue from 'vue'
import axios from 'axios'
import router from '#/router'
let config = {
baseURL : process.env.VUE_APP_BASE_API,
timeout : 30000,
headers : {
Accept : 'application/json',
'Content-Type' : 'application/json',
},
}
const GET = (url, params) => REQUEST({ method: 'get', url, params })
const POST = (url, data) => REQUEST({ method: 'post', url, data })
const PUT = (url, data) => REQUEST({ method: 'put', url, data })
const PATCH = (url, data) => REQUEST({ method: 'patch', url, data })
const DELETE = url => REQUEST({ method: 'delete', url })
const REQUEST = conf => {
conf = { ...conf, ...config }
conf = setAccessTokenHeader(conf)
return new Promise((resolve, reject) => {
axios
.request(conf)
.then(response => {
resolve(response.data)
})
.catch(error => {
outputError(error)
reject(error)
})
})
}
function setAccessTokenHeader (config) {
const access_token = Vue.cookie.get('access_token')
if (access_token) {
config.headers.Authorization = 'Bearer ' + access_token
}
return config
}
/* https://github.com/axios/axios#handling-errors */
function outputError (error) {
if (error.response) {
/**
* The request was made and the server responded with a
* status code that falls out of the range of 2xx
*/
if (error.response.status === 401) {
router.replace({ name: 'login' })
return
}
else {
/* other response status such as 403, 404, 422, etc */
}
}
else if (error.request) {
/**
* The request was made but no response was received
* `error.request` is an instance of XMLHttpRequest in the browser
* and an instance of http.ClientRequest in node.js
*/
}
else {
/* Something happened in setting up the request that triggered an Error */
}
}
export default {
GET,
POST,
DELETE,
PUT,
PATCH,
REQUEST,
}
You could use an interceptor with axios. Catch the 401s and clear the local storage when you do then redirect user to appropriate page.
I'm following this tutorial, currently I can log in and out with a user but when a user logs in the JWT token isn't send with the header request (I think) so I get a 401 after the router.navigate. When I reload the page I can use the token and everything works.
In my login.component.ts I have this login function:
login() {
this.loading = true;
this.authenticationService.login(this.model.username, this.model.password)
.subscribe(result => {
if (result === true) {
// login successful
this.router.navigate(['home']);
} else {
// login failed
this.error = 'Username or password is incorrect';
this.loading = false;
}
}, error => {
this.loading = false;
this.error = error;
});
}
This calls the login function in the authentication.service.ts:
login(username: string, password: string): Observable<boolean> {
return this.http.post(this.authUrl, JSON.stringify({username: username, password: password}), {headers: this.headers})
.map((response: Response) => {
// login successful if there's a jwt token in the response
const token = response.json() && response.json().token;
if (token) {
// store username and jwt token in local storage to keep user logged in between page refreshes
localStorage.setItem('currentUser', JSON.stringify({ username: username, token: token }));
// return true to indicate successful login
alert('Success');
return true;
} else {
// return false to indicate failed login
alert('Fail');
return false;
}
}).catch((error: any) => Observable.throw(error.json().error || 'Server error'));
}
If the login is successful the user is routed to /home:
this.router.navigate(['home']);
In the home.component.ts I have a getAll function that returns all movies in the database:
getAll() {
this._dataService
.getAll<Movie[]>()
.subscribe((data: any[]) => this.movies = data,
error => () => {
'something went wrong';
},
() => {
console.log(this.movies);
});
}
This function is called on the ngOnInit:
ngOnInit(): void {
this.getAll();
}
In my app.service.ts I have the get function:
public getAll<T>(): Observable<T[]> {
if (this.authenticationService.getToken()) {
console.log(this.authenticationService.getToken());
console.log(this.headers);
return this.http.get<T[]>('/api/movies/all', {headers: this.headers});
}
}
But when I log in I get this error after being routed to the home page:
GET http://localhost:4200/api/movies/all 401 (Unauthorized)
The problem (I think) is that when I get routed to the home page the header is missing the token. But as you can see from the console log the token is available in app.service.ts.
When I reload the page I do have the token set in the header and everything works:
Any ideas on how to expose the token to the header after the redirect?
//EDIT
For some reason I do get the JWT token when I set the header directly in the function:
return this.http.get<T[]>('/api/movies/all', {headers: new HttpHeaders().set('Authorization', 'Bearer ' + this.authenticationService.getToken())});
Instead of calling it like this:
headers = new HttpHeaders().set('Authorization', 'Bearer ' + this.authenticationService.getToken());
return this.http.get('/api/movies/' + id, {headers: this.headers});
I have created a custom module in Drupal 8 that allows a user to authenticate using facebook login. Their access token is checked against one stored in the database and if it matches authenticates the user and if it doesn't then redirects them to a page that allows them to link their Facebook account to a Drupal user.
The button for login is:
<button id="login_fb" onclick="logIt()">Log in with Facebook</button>
The "logit" function with the ajax request to the Drupal controller is:
function logIt()
{
FB.login(function(response) {
if (response.authResponse) {
if(response.authResponse.accessToken)
{
var request = $.ajax({
url: "/user/token",
method: "POST",
data: { access_token : response.authResponse.accessToken},
dataType: "json"
});
request.done(function( msg ) {
window.location.replace(msg['redirect_url']);
});
request.fail(function( jqXHR, textStatus ) {
alert( "Request failed: " + textStatus );
});
}
}
}
And the controller code that handles this ajax call is:
public function token() {
$fb_token = $_POST['access_token'];
$query = db_select('user__field_fb_token', 'u');
$query
->fields('u')
->condition('u.field_fb_token_value', $fb_token,'=');
$res = $query->execute();
$res->allowRowCount = TRUE;
$count = $res->rowCount();
//See if anybody has this access token
if($count > 0)
{
$user = $res->fetchAssoc();
//TODO: Refresh access token and update
$login_id = $user['entity_id'];
//Redirect the user to topics
user_login_finalize(user_load($login_id));
$response_arr = array("status" => "authorised","redirect_url" => "/topics");
}
else
{
$_SESSION['access_token'] = $fb_token;
$response_arr = array("status" => "unauthorised","redirect_url" => "/user/auth","token" => $fb_token);
}
$response = new Response();
$response->setContent(json_encode($response_arr));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
The weird thing is that the db_select query always returns 0 and therefore does not authenticate the user account that has this token. However replacing
$fb_token = $_POST['access_token'];
with
$fb_token = '** hard coded access token **';
yields the correct result. I have checked that the post variable being passed in and it is present (that's why I pass it back with the unauthorised response to check that it's not blank).
I think it may have something to do with the async nature of FB.Login method but not sure.
Any help on this matter would be greatly appreciated!
I am extending Ember Simple Auth's base authentication class to allow authentication with Google. So far, it works on Safari 8 and Chrome 41 (both on Yosemite) with no errors. However, on Firefox 35, it throws an Error that does not occur on the other browsers. Here is my Google authenticator class:
App.GoogleAuthenticator = SimpleAuth.Authenticators.Base.extend({
// constants for Google API
GAPI_CLIENT_ID: 'the client id',
GAPI_SCOPE: ['email'],
GAPI_TOKEN_VERIFICATION_ENDPOINT: 'https://www.googleapis.com/oauth2/v2/tokeninfo',
// method for scheduleing a single token refresh
// time in milliseconds
scheduleSingleTokenRefresh: function(time) {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.run.later(self, function() {
gapi.auth.authorize({
client_id: self.GAPI_CLIENT_ID,
scope: self.GAPI_SCOPE,
immediate: true
}, function(data) {
if (data && !data.error) {
resolve(data);
} else {
reject((data || {}).error);
}
});
}, time);
});
},
// WIP: recursive method that reschedules another token refresh after the previous scheduled one was fulfilled
// usage: scheduleTokenRefreshes(time until token should refresh for the first time, time between subsequent refreshes)
// usage: scheduleTokenRefreshes(time between refreshes)
scheduleTokenRefreshes: function(time1, time2) {
var self = this;
// if there is a time2, schedule a single refresh, wait for it to be fulfilled, then call myself to schedule again
if (!Ember.isEmpty(time2)) {
self.scheduleSingleTokenRefresh(time1)
.then(function() {
self.scheduleTokenRefreshes(time2);
});
// if there isn't a time2, simply schedule a single refresh, then call myself to schedule again
} else {
self.scheduleSingleTokenRefresh(time1)
.then(function() {
self.scheduleTokenRefreshes(time1);
});
}
},
// method that restores the session on reload
restore: function(data) {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
console.log(data);
if (Ember.isEmpty(data.access_token)) {
reject();
return;
}
// schedule a refresh 15 minutes before it expires or immediately if it expires in < 15
var timeNow = Math.floor(Date.now() / 1000);
var expiresAt = +data.expires_at;
var timeDifference = expiresAt - timeNow;
var schedulingDelay = Math.floor(timeDifference - 15 * 60);
schedulingDelay = schedulingDelay < 0 ? 0 : schedulingDelay;
self.scheduleTokenRefreshes(schedulingDelay * 1000, 45 * 60);
resolve(data);
});
},
// method that authenticates
authenticate: function() {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
gapi.auth.authorize({
client_id: self.GAPI_CLIENT_ID,
scope: self.GAPI_SCOPE
}, function(data) {
if (data && !data.error) {
// schedule a refresh in 45 minutes
var schedulingDelay = 45 * 60;
self.scheduleTokenRefreshes(schedulingDelay * 1000);
resolve(data);
} else {
reject((data || {}).error);
}
});
});
},
// method that logs the user out and revokes the token
invalidate: function(data) {
var self = this;
return new Ember.RSVP.Promise(function(resolve, reject) {
// send a GET request to revoke the token
Ember.$.ajax({
type: 'GET',
url: 'https://accounts.google.com/o/oauth2/revoke?token=' + self.get('session.access_token'),
contentType: 'application/json',
dataType: 'jsonp'
})
.done(function(successData) {
resolve(successData);
})
.fail(function(error) {
reject(error);
});
});
}
});
When the popup window closes after a successful login on Google's end, this error appears on Firefox's console:
Error: Assertion Failed: Error: Permission denied to access property 'toJSON' ember.js:13749
"__exports__.default<.persist#http://127.0.0.1/~jonchan/test/bower_components/ember-simple-auth/simple-auth.js:1524:1
__exports__.default<.updateStore#http://127.0.0.1/~jonchan/test/bower_components/ember-simple-auth/simple-auth.js:1195:11
__exports__.default<.setup#http://127.0.0.1/~jonchan/test/bower_components/ember-simple-auth/simple-auth.js:1149:9
__exports__.default<.authenticate/</<#http://127.0.0.1/~jonchan/test/bower_components/ember-simple-auth/simple-auth.js:1066:13
tryCatch#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:47982:16
invokeCallback#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:47994:17
publish#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:47965:11
#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:29462:9
Queue.prototype.invoke#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:848:11
Queue.prototype.flush#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:913:13
DeferredActionQueues.prototype.flush#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:718:13
Backburner.prototype.end#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:143:11
createAutorun/backburner._autorun<#http://127.0.0.1/~jonchan/test/bower_components/ember/ember.js:546:9
" ember.js:29488
Here is the version information:
DEBUG: Ember : 1.9.1
DEBUG: Ember Data : 1.0.0-beta.14.1
DEBUG: Handlebars : 2.0.0
DEBUG: jQuery : 2.1.3
DEBUG: Ember Simple Auth : 0.7.2
The most confounding thing is that this only appears on Firefox. Is it a bug in Ember Simple Auth or Ember? How do I fix it?
I do not know about only Firefox throwing an error (I've had a similar error with Chrome 40), but there is a bug in ember-simple-auth 0.7.2 with Ember 1.9 that prohibits sending an actual error response in the authenticate method in the authenticator.
If you return reject() in the rejection function of authenticate it will not throw an additional error. This will however not propagate the errorstatus or message, so I consider this a bug.
A work-around was proposed on github about this issue by setting Ember.onerror=Ember.K temporarily so additional errors will not be propagated, although it will propagate the original authenticate rejection with the error-status.
The issue in the github repo only mentions problems with testing this, but I've had this problem in normal code.
see: https://github.com/simplabs/ember-simple-auth/issues/407
Turns out the error was on the resolve part of the authenticate method. Here is what fixed it:
App.GoogleAuthenticator = SimpleAuth.Authenticators.Base.extend({
authenticate: function() {
return new Ember.RSVP.Promise(function(resolve, reject) {
gapi.auth.authorize({
client_id: 'the client id',
scope: ['the scopes'],
}, function(data) {
if (data && !data.error) {
resolve({
access_token: data.access_token // !! passing the entire 'data' object caused the error somehow
});
} else {
reject((data || {}).error);
}
});
});
},
// ...
});
I'm still not quite sure why this caused the error. Perhaps the Google API's response (in its entirety) is somehow incompatible with Ember Simple Auth.