DEV Community

Cover image for Securely Storing JWTs in (Flutter) Web Apps
Carmine Zaccagnino
Carmine Zaccagnino

Posted on

19 3 1 1

Securely Storing JWTs in (Flutter) Web Apps

I recently wrote a post about how to implement JWT Authorization in Flutter apps. I only considered the use case of writing a mobile app, so I recommended the use of the flutter_secure_storage package to store the tokens.

As it later emerged, some people wanted to use that tutorial as a guide for Flutter Web apps. flutter_secure_storage doesn't work for Web apps.

This first post about this topic will simply look to address that, later I'll post a more general overview of what needs to be taken in consideration when writing cross-platform apps in Flutter, including a deeper dive into storage on the different platforms. This will be more of a hands-on tutorial like the ones I previously posted, whereas the other one will be more of an opinion/overview post that I hope will help understand the thinking that is required before trying to deploy on multiple platforms.

For the sake of keeping each post focused, I'll keep this one more practical and focused on how to make that example work on the Web.

The Basic, Compromising, Simple Approach

The simple solution to that problem that will work on every platform is to use the shared_preferences package, which uses SharedPreferences on Android, NSUserDefaults on iOS and localStorage on the Web.

This is actually only one half of a solution, as using localStorage means that the value can be retrieved by any JS (or Dart code compiled to JS like Flutter Web code) on your page. This means you have to be extra careful with user input to avoid XSS attacks, and that comes into play especially if you have non-Flutter Web content on the same domain (which can access the same localStorage), which can access the same data.

Also, you need to keep in mind that the user can actually very easily access the token through JavaScript, so if their end gets compromised in any way that token can be easily read.

This should already give you an idea of the kind of issues you might have to deal with when writing cross-platform apps. Also, especially NSUserDefaults on iOS is not supposed to be used for sensitive information (nor is SharedPreferences actually), so you should use flutter_secure_storage on those platforms anyway, and at that point you can just use dart:html directly for the Web part and skip having shared_preferences as a dependency entirely.

The Better Approach

flutter_secure_storage on mobile should be your first and only choice. It uses the proper Keychain API on iOS and it encrypts the data, stores the encrypted data in SharedPreferences and the cryptographic key is stored in the Android KeyStore, which is a safe approach.

On the Web though, you need to use a Web-based solution, so you need to think about our Flutter app as if it was any old boring HTML, CSS and JavaScript website.

The place where tokens are stored in Web apps are httpOnly cookies, which are sent to the backend automatically along with each request, but aren't accessible by JavaScript.

The issue with that is that automatically part. It makes you vulnerable to CSRF, which is an attack that sends requests to your server from one of your users clients when they visit a page that isn't yours. GET requests are particularly vulnerable because a GET request query is just a link that can be clicked by your users.

This can be prevented by having another token (which shouldn't be the same token you use for authorization, obviously, but it can be generated based on that) that is stored in localStorage (so only your website's code can access it) and send that along with each request in a dedicated header.

You still MUST NOT be vulnerable to XSS because, even though your token can't be read, an attacker can still send requests through your own page, take your CSRF token, and bypass your CSRF protection entirely, but at least the JWT isn't in their hands so they haven't permanently stolen your user's identity. Escape all user-provided data before ever doing anything with it both on the front-end if you have another website running and on the back-end when you put data in a database: many libraries to interact with databases have that feature built-in, you just need to use them properly instead of just concatenating strings. If you really have to use string concatenation because of your stack, remember to sanitize the input before doing anything with it.

Now, let's get hands-on and see the code needed to make everything work!

Putting It In Practice

If you're new to my posts (and there will be a few of you), you might not be aware of my earlier post about JWT authentication with Flutter and Node (which supposed you were writing a mobile app), which I'll use as a starting point, and that you therefore should at least be aware of in case something confuses you. I won't be starting from scratch here.

The Node Side

Before we can build the app, we need to get the backend API straight. Remember, I won't explain the stuff I already covered in my previous post. The starting point is this repository

GitHub logo carzacc / jwt-tutorial-backend

Backend for a blog post about User Authentication+JWT authorization with Node and Flutter

.

The code we're going to end up with is in this repository

.

The usual Node imports and the signup route don't require any changes:

var express = require('express');
var jwt = require('jsonwebtoken');
var sqlite = require('sqlite3');
var crypto = require('crypto');
var cookieParser = require("cookie-parser");
const KEY = "m yincredibl y(!!1!11!)<'SECRET>)Key'!";
var db = new sqlite.Database("users.sqlite3");
var app = express();
// routes
let port = process.env.PORT || 3000;
app.listen(port, function () {
return console.log("Started user authentication server listening on port " + port);
});
app.post('/signup', express.urlencoded(), function(req, res) {
// in a production environment you would ideally add salt and store that in the database as well
// or even use bcrypt instead of sha256. No need for external libs with sha256 though
var password = crypto.createHash('sha256').update(req.body.password).digest('hex');
db.get("SELECT FROM users WHERE username = ?", [req.body.username], function(err, row) {
if(row != undefined ) {
console.error("can't create user " + req.body.username);
res.status(409);
res.send("An user with that username already exists");
} else {
console.log("Can create user " + req.body.username);
db.run('INSERT INTO users(username, password) VALUES (?, ?)', [req.body.username, password]);
res.status(201);
res.send("Success");
}
});
});
view raw index_signup.js hosted with ❤ by GitHub

The login route, though, is where the fun part starts (you're reading this because you find this all fun, don't you?). The way I'm going to do this is the following: I'm going to generate two tokens: an access token and a an anti-CSRF token, and we're going to send the first as a cookie and the second in the response body. I'm not setting the Secure flag because this code isn't meant for production, but that should be set in production code given that your app would run with TLS in a production environment:

app.post('/login', express.urlencoded(), function(req, res) {
console.log(req.body.username + " attempted login");
var password = crypto.createHash('sha256').update(req.body.password).digest('hex');
db.get("SELECT * FROM users WHERE (username, password) = (?, ?)", [req.body.username, password], function(err, row) {
if(row != undefined ) {
var payload = {
username: req.body.username,
type: 'access'
};
var csrfPayload = {
username: req.body.username,
type: 'csrf'
};
var token = jwt.sign(payload, KEY, {algorithm: 'HS256', expiresIn: "15 days"});
var csrf = jwt.sign(csrfPayload, KEY, {algorithm: 'HS256', expiresIn: "15 days"});
console.log("Success");
res.cookie('jwt', token, {magAge: 15*24*60*60*1000, httpOnly: true/*, secure: true */});
res.send(csrf);
} else {
console.error("Failure");
res.status(401)
res.send("There's no user matching that");
}
});
});
view raw index_login.js hosted with ❤ by GitHub

The data route needs to get both values, one in the cookies and one as a header in order to work:

app.get('/data', cookieParser(), function(req, res) {
var csrf = req.get('CSRF');
var str = req.cookies['jwt'];
try {
let jwtPayload = jwt.verify(str, KEY);
let csrfPayload = jwt.verify(csrf, KEY);
if(jwtPayload["type"] != 'access')
throw "invalid jwt payload";
if(csrfPayload["type"] != 'csrf')
throw "invalid anti-CSRF token payload"
res.send("Very Secret Data");
} catch(e) {
console.error(e);
res.status(401);
res.send("Bad Token");
}
view raw index_data.js hosted with ❤ by GitHub

and here is the entire index.js:

var express = require('express');
var jwt = require('jsonwebtoken');
var sqlite = require('sqlite3');
var crypto = require('crypto');
var cookieParser = require("cookie-parser");
const KEY = "m yincredibl y(!!1!11!)<'SECRET>)Key'!";
var db = new sqlite.Database("users.sqlite3");
var app = express();
app.post('/signup', express.urlencoded(), function(req, res) {
// in a production environment you would ideally add salt and store that in the database as well
// or even use bcrypt instead of sha256. No need for external libs with sha256 though
var password = crypto.createHash('sha256').update(req.body.password).digest('hex');
db.get("SELECT FROM users WHERE username = ?", [req.body.username], function(err, row) {
if(row != undefined ) {
console.error("can't create user " + req.body.username);
res.status(409);
res.send("An user with that username already exists");
} else {
console.log("Can create user " + req.body.username);
db.run('INSERT INTO users(username, password) VALUES (?, ?)', [req.body.username, password]);
res.status(201);
res.send("Success");
}
});
});
app.post('/login', express.urlencoded(), function(req, res) {
console.log(req.body.username + " attempted login");
var password = crypto.createHash('sha256').update(req.body.password).digest('hex');
db.get("SELECT * FROM users WHERE (username, password) = (?, ?)", [req.body.username, password], function(err, row) {
if(row != undefined ) {
var payload = {
username: req.body.username,
type: 'access'
};
var csrfPayload = {
username: req.body.username,
type: 'csrf'
};
var token = jwt.sign(payload, KEY, {algorithm: 'HS256', expiresIn: "15 days"});
var csrf = jwt.sign(csrfPayload, KEY, {algorithm: 'HS256', expiresIn: "15 days"});
console.log("Success");
res.cookie('jwt', token, {magAge: 15*24*60*60*1000, httpOnly: true/*, secure: true */});
res.send(csrf);
} else {
console.error("Failure");
res.status(401)
res.send("There's no user matching that");
}
});
});
app.get('/data', cookieParser(), function(req, res) {
var csrf = req.get('CSRF');
var str = req.cookies['jwt'];
try {
let jwtPayload = jwt.verify(str, KEY);
let csrfPayload = jwt.verify(csrf, KEY);
if(jwtPayload["type"] != 'access')
throw "invalid jwt payload";
if(csrfPayload["type"] != 'csrf')
throw "invalid anti-CSRF token payload"
res.send("Very Secret Data");
} catch(e) {
console.error(e);
res.status(401);
res.send("Bad Token");
}
});
let port = process.env.PORT || 3000;
app.listen(port, function () {
return console.log("Started user authentication server listening on port " + port);
});
view raw index.js hosted with ❤ by GitHub

The Flutter Side

On the Flutter side, the starting point is this repository

GitHub logo carzacc / jwt-tutorial-flutter

https://carmine.dev/posts/jwtauth/

.

The code we're going to end up with is in this repository

.

The approach is going to be the following, in order to make it as obvious as possible we're actually building a Web app: the JWT is going to be in the cookies, so it's beyond our control, whereas we're going to store the anti-CSRF token in the localStorage directly using dart:html.

This means that we are going to add to our imports import 'dart:html' show window; and take out the flutter_secure_storage dependency given that we are not using it:

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:html' show window;
import 'dart:convert' show json, base64, ascii;
const SERVER_IP = 'http://localhost:5000';
void main() {
runApp(MyApp());
}

MyApp is, as always, where we decide whether we need to show the data page or the login page. In order to change as little as possible from the original, I switched the home from a FutureBuilder to a Builder (we don't need Futures in the case of HTML localStorage) and moving from calling a jwtOrEmpty getter to generating our very own csrfTokenOrEmpty. The rest stays the same: if it's expired, the access token has expired too, so we need the user to log in again, if we don't have one we need to show the user the login page:

class MyApp extends StatelessWidget {
MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Authentication Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Builder(
builder: (context) {
var csrfTokenOrEmpty = window.localStorage.containsKey("csrf") ? window.localStorage["csrf"] : "";;
if(csrfTokenOrEmpty != "") {
var str = csrfTokenOrEmpty;
var token = str.split(".");
if(token.length !=3) {
window.localStorage.remove("csrf");
return LoginPage();
} else {
var payload = json.decode(ascii.decode(base64.decode(base64.normalize(token[1]))));
if(DateTime.fromMillisecondsSinceEpoch(payload["exp"]*1000).isAfter(DateTime.now())) {
return HomePage(str, payload);
} else {
window.localStorage.remove("csrf");
return LoginPage();
}
}
} else {
window.localStorage.remove("csrf");
return LoginPage();
}
}
),
);
}
}
view raw main_myapp.dart hosted with ❤ by GitHub

The LoginPage gets changes in the callback to log in, as that needs to store the JWT to local storage too.

window.localStorage can be accessed just like any Map like you see in the following snippet we're going to use in the login page:



var username = _usernameController.text;
var password = _passwordController.text;
var jwt = await attemptLogIn(username, password);
if(jwt != null) {
  window.localStorage["csrf"] = jwt;
  Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => HomePage.fromBase64(jwt)
  )
);


Enter fullscreen mode Exit fullscreen mode

I kept the variable names as they were for maximum comparability with the previous post.

here's the entire LoginPage:

class LoginPage extends StatelessWidget {
LoginPage();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
void displayDialog(context, title, text) => showDialog(
context: context,
builder: (context) =>
AlertDialog(
title: Text(title),
content: Text(text)
),
);
Future<String> attemptLogIn(String username, String password) async {
var res = await http.post(
"$SERVER_IP/login",
body: {
"username": username,
"password": password
}
);
if(res.statusCode == 200) return res.body;
return null;
}
Future<int> attemptSignUp(String username, String password) async {
var res = await http.post(
'$SERVER_IP/signup',
body: {
"username": username,
"password": password
}
);
return res.statusCode;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Log In"),),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Username'
),
),
TextField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password'
),
),
FlatButton(
onPressed: () async {
var username = _usernameController.text;
var password = _passwordController.text;
var jwt = await attemptLogIn(username, password);
if(jwt != null) {
window.localStorage["csrf"] = jwt;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomePage.fromBase64(jwt)
)
);
} else {
displayDialog(context, "An Error Occurred", "No account was found matching that username and password");
}
},
child: Text("Log In")
),
FlatButton(
onPressed: () async {
var username = _usernameController.text;
var password = _passwordController.text;
if(username.length < 4)
displayDialog(context, "Invalid Username", "The username should be at least 4 characters long");
else if(password.length < 4)
displayDialog(context, "Invalid Password", "The password should be at least 4 characters long");
else{
var res = await attemptSignUp(username, password);
if(res == 201)
displayDialog(context, "Success", "The user was created. Log in now.");
else if(res == 409)
displayDialog(context, "That username is already registered", "Please try to sign up using another username or log in if you already have an account.");
else {
displayDialog(context, "Error", "An unknown error occurred.");
}
}
},
child: Text("Sign Up")
)
],
),
)
);
}
}
view raw main_login.dart hosted with ❤ by GitHub

The data (home) page is going to be the simplest one, as all that needs to change is the name of the header at which we send the anti-CSRF token, given that the access token is sent along with the request as a cookie automatically and the rest it taken care of by the backend:

class HomePage extends StatelessWidget {
HomePage(this.jwt, this.payload);
factory HomePage.fromBase64(String jwt) =>
HomePage(
jwt,
json.decode(
ascii.decode(
base64.decode(base64.normalize(jwt.split(".")[1]))
)
)
);
final String jwt;
final Map<String, dynamic> payload;
@override
Widget build(BuildContext context) =>
Scaffold(
appBar: AppBar(title: Text("Secret Data Screen")),
body: Center(
child: FutureBuilder(
future: http.read('$SERVER_IP/data', headers: {"CSRF": jwt}),
builder: (context, snapshot) =>
snapshot.hasData ?
Column(children: <Widget>[
Text("${payload['username']}, here's the data:"),
Text(snapshot.data, style: Theme.of(context).textTheme.headline4)
],)
:
snapshot.hasError ? Text("An error occurred") : CircularProgressIndicator()
),
),
);
}

Here's the full main.dart for that:

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:html' show window;
import 'dart:convert' show json, base64, ascii;
const SERVER_IP = 'http://localhost:5000';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Authentication Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Builder(
builder: (context) {
var csrfTokenOrEmpty = window.localStorage.containsKey("csrf") ? window.localStorage["csrf"] : "";;
if(csrfTokenOrEmpty != "") {
var str = csrfTokenOrEmpty;
var token = str.split(".");
if(token.length !=3) {
window.localStorage.remove("csrf");
return LoginPage();
} else {
var payload = json.decode(ascii.decode(base64.decode(base64.normalize(token[1]))));
if(DateTime.fromMillisecondsSinceEpoch(payload["exp"]*1000).isAfter(DateTime.now())) {
return HomePage(str, payload);
} else {
window.localStorage.remove("csrf");
return LoginPage();
}
}
} else {
window.localStorage.remove("csrf");
return LoginPage();
}
}
),
);
}
}
class LoginPage extends StatelessWidget {
LoginPage();
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
void displayDialog(context, title, text) => showDialog(
context: context,
builder: (context) =>
AlertDialog(
title: Text(title),
content: Text(text)
),
);
Future<String> attemptLogIn(String username, String password) async {
var res = await http.post(
"$SERVER_IP/login",
body: {
"username": username,
"password": password
}
);
if(res.statusCode == 200) return res.body;
return null;
}
Future<int> attemptSignUp(String username, String password) async {
var res = await http.post(
'$SERVER_IP/signup',
body: {
"username": username,
"password": password
}
);
return res.statusCode;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Log In"),),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: <Widget>[
TextField(
controller: _usernameController,
decoration: InputDecoration(
labelText: 'Username'
),
),
TextField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Password'
),
),
FlatButton(
onPressed: () async {
var username = _usernameController.text;
var password = _passwordController.text;
var jwt = await attemptLogIn(username, password);
if(jwt != null) {
window.localStorage["csrf"] = jwt;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomePage.fromBase64(jwt)
)
);
} else {
displayDialog(context, "An Error Occurred", "No account was found matching that username and password");
}
},
child: Text("Log In")
),
FlatButton(
onPressed: () async {
var username = _usernameController.text;
var password = _passwordController.text;
if(username.length < 4)
displayDialog(context, "Invalid Username", "The username should be at least 4 characters long");
else if(password.length < 4)
displayDialog(context, "Invalid Password", "The password should be at least 4 characters long");
else{
var res = await attemptSignUp(username, password);
if(res == 201)
displayDialog(context, "Success", "The user was created. Log in now.");
else if(res == 409)
displayDialog(context, "That username is already registered", "Please try to sign up using another username or log in if you already have an account.");
else {
displayDialog(context, "Error", "An unknown error occurred.");
}
}
},
child: Text("Sign Up")
)
],
),
)
);
}
}
class HomePage extends StatelessWidget {
HomePage(this.jwt, this.payload);
factory HomePage.fromBase64(String jwt) =>
HomePage(
jwt,
json.decode(
ascii.decode(
base64.decode(base64.normalize(jwt.split(".")[1]))
)
)
);
final String jwt;
final Map<String, dynamic> payload;
@override
Widget build(BuildContext context) =>
Scaffold(
appBar: AppBar(title: Text("Secret Data Screen")),
body: Center(
child: FutureBuilder(
future: http.read('$SERVER_IP/data', headers: {"CSRF": jwt}),
builder: (context, snapshot) =>
snapshot.hasData ?
Column(children: <Widget>[
Text("${payload['username']}, here's the data:"),
Text(snapshot.data, style: Theme.of(context).textTheme.headline4)
],)
:
snapshot.hasError ? Text("An error occurred") : CircularProgressIndicator()
),
),
);
}
view raw main.dart hosted with ❤ by GitHub

In order to make that Flutter app look and behave nicer on bigger screens and browsers in general, you might want to check out the tutorial on responsive and Web development with Flutter I wrote for Smashing Magazine.

Onwards: Your Next Steps

You might have found this post useful, and perhaps you'd like to see more Flutter content from me. What you can do is:

AWS GenAI LIVE image

Real challenges. Real solutions. Real talk.

From technical discussions to philosophical debates, AWS and AWS Partners examine the impact and evolution of gen AI.

Learn more

Top comments (19)

Collapse
 
chitgoks profile image
chitgoks

hi im following this thread and its working in android but in flutter web, the response does not have any cookie in the header.

only content l3ngth and type are available. i tried dio and http, same result.

in express i tried httpOnly false and true.

thoughts?

Collapse
 
carminezacc profile image
Carmine Zaccagnino

Where are you trying to acess the cookies from? From the Flutter Dart code or from the Node backend code?

Collapse
 
chitgoks profile image
chitgoks

from dart code.

Thread Thread
 
carminezacc profile image
Carmine Zaccagnino • Edited

As I said in the post:

The place where tokens are stored in Web apps are httpOnly cookies, which are sent to the backend automatically along with each request, but aren't accessible by JavaScript.

In the case of a Flutter app and nor a traditional Web app JavaScript is replaced by Dart.

The whole point of having cookies (especially if httpOnly) is that you don't need to access them on the frontend, as they're automatically sent to the backend as I showed in the post.

The only stuff you should be worrying about accessing in the frontend is the stuff you want to put in localStorage.

Hope this helps.

Thread Thread
 
chitgoks profile image
chitgoks

so you mean that if its an app, the cookie should be saved. but if it is web, the cookie is automatically included?

since browser automatically keeps cookies?

Thread Thread
 
carminezacc profile image
Carmine Zaccagnino • Edited

Yes, exactly.

Also, the cookies are saved automatically in the document.cookie just like they would if you were writing regular JS.
Unless they're httpOnly: in that case the frontend can never access them so they can be accessed only by the backend when you send a request.

Thread Thread
 
chitgoks profile image
chitgoks

cool. ill check.

i can understand that but somehow its weird that the response header doesnt show the cookie after login.

Thread Thread
 
chitgoks profile image
chitgoks

hi carmine. it seems that the problem is req.cookies returns null in the backend when flutter web sends a request to the backend.

also set httpOnly to false so i could see document.cookie contents but nothing is saved to the browser. weird.

Thread Thread
 
jsonpoindexter profile image
Jason Poindexter

I am also having the same issue. I can see the Set-Cookie header in the login response but the cookie is not actually being set

Thread Thread
 
carminezacc profile image
Carmine Zaccagnino

@chitgoks and @jsonpoindexter I've noticed that. Google's HTTP library seems to not retain cookies sometimes. Switching to the dio http library should fix it in my experience, and Dio's API is very close to Google's. I'm sorry for the late response but I've not been loggin in to dev.to often lately.

Thread Thread
 
jsonpoindexter profile image

Thank you for taking the time to respond @carminezacc ! What ended up working for me was setting the withCredentials parameter for the BrowserClient to true (it is defaulted to false). After that, my browser did all the cookie management!
github.com/dart-lang/http/blob/20e...

Collapse
 
dude6363 profile image
Dude6363

hi.
Flutter_secure_storage and Shared_preferences and Flutter_Session are save data in local storage.
but JWT Token save data in cookies.
Which once is better and safe?
do we need JWT token when we use Flutter_secure_storage or not?

Collapse
 
carminezacc profile image
Carmine Zaccagnino

Sorry to answer so late, but I haven't logged in to DEV for really long. Cookies aren't safe from CSRF, localStorage is as safe as your frontend code. With Flutter you might not have much to worry about, but XSS on the Web is still an issue for some websites, that's why one should ideally use a different token in each and have the backend require both.

Collapse
 
dude6363 profile image
Dude6363

Thanks for your Answer,
I use to flutter secure storage in flutter web.
My problem is local storage web browser. if attacker change my token in local storage with XSS ,flutter secure storage should log out but it can not?

Thread Thread
 
carminezacc profile image
Carmine Zaccagnino

If your backend identifies the user through both a token in local storage and a different one in HttpOnly cookies (which can't be accessed directly by scripts running on webpages) it can verify both are present and matching. The HttpOnly cookie defends from XSS (by not being accessible to scripts) and the local storage token protects from CSRF because only scripts running on your website can access it.

Thread Thread
 
dude6363 profile image
Dude6363

token generate in backend and sent to flutter secure storage.
flutter secure storage get token and saved token in local storage .
but when you change the token in local storage of browser,flutter web got error (error: formatexception: invalid length, must be multiple of four (at character 16) in flutter secure storage),

what should I do for this error?

Collapse
 
ariel_leventhal profile image
portalgamesmais

Im trying to follow this but cookies are not being set on chrome. Its my fisrt time developing for web so I dont exactly know whats the problem, according to my research appenretly its a problem with cors. I have already tried setting credentials to true both on node and flutter side and also I tried using dio. Nothing seems to work.

Collapse
 
baechirhoon profile image
baechirhoon

this is great tutorial
can i have one question?
how to make logout?

i tried like these
jwt.sign(payload, KEY, {algorithm: 'HS256', expiresIn: "0"});
jwt.sign(payload, KEY, {algorithm: 'HS256', expiresIn: "-1d"});
failed

because i found expire date method log out

how to do?

Collapse
 
elya29 profile image
Elya

Hi, you can clear the cookies and the local storage to log out.

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay