I have implemented JWT for user login in my app (before Spotify Auth), like so:
@auth_blueprint.route('/auth/login', methods=['POST'])
def login_user():
# get post data
post_data = request.get_json()
response_object = {
'status': 'fail',
'message': 'Invalid payload.'
}
if not post_data:
return jsonify(response_object), 400
email = post_data.get('email')
password = post_data.get('password')
try:
# fetch the user data
user = User.query.filter_by(email=email).first()
if user and bcrypt.check_password_hash(user.password, password):
auth_token = user.encode_auth_token(user.id)
if auth_token:
response_object['status'] = 'success'
response_object['message'] = 'Successfully logged in.'
response_object['auth_token'] = auth_token.decode()
return jsonify(response_object), 200
else:
response_object['message'] = 'User does not exist.'
return jsonify(response_object), 404
except Exception:
response_object['message'] = 'Try again.'
return jsonify(response_object), 500
These are the methods of my SQLAlchemy User(db.Model)
def encode_auth_token(self, user_id):
"""Generates the auth token"""
try:
payload = {
'exp': datetime.datetime.utcnow() + datetime.timedelta(
days=current_app.config.get('TOKEN_EXPIRATION_DAYS'),
seconds=current_app.config.get('TOKEN_EXPIRATION_SECONDS')
),
'iat': datetime.datetime.utcnow(),
'sub': user_id
}
return jwt.encode(
payload,
current_app.config.get('SECRET_KEY'),
algorithm='HS256'
)
except Exception as e:
return e
@staticmethod
def decode_auth_token(auth_token):
"""
Decodes the auth token - :param auth_token: - :return: integer|string
"""
try:
payload = jwt.decode(
auth_token, current_app.config.get('SECRET_KEY'))
return payload['sub']
except jwt.ExpiredSignatureError:
return 'Signature expired. Please log in again.'
except jwt.InvalidTokenError:
return 'Invalid token. Please log in again.'
App.jsx
loginUser(token) {
window.localStorage.setItem('authToken', token);
this.setState({ isAuthenticated: true });
this.getUsers();
this.createMessage('Welcome', 'success');
};
(...)
<Route exact path='/login' render={() => (
<Form
isAuthenticated={this.state.isAuthenticated}
loginUser={this.loginUser}
/>
)} />
and
Form.jsx
handleUserFormSubmit(event) {
event.preventDefault();
const data = {
email: this.state.formData.email,
password: this.state.formData.password
};
const url = `${process.env.REACT_APP_WEB_SERVICE_URL}/auth/${formType.toLowerCase()}`;
axios.post(url, data)
.then((res) => {
this.props.loginUser(res.data.auth_token);
})
Now I'd like to add a second layer of authentication and handle tokens after Spotify callback, like so:
@spotify_auth_bp.route("/callback", methods=['GET', 'POST'])
def spotify_callback():
# Auth Step 4: Requests refresh and access tokens
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
REDIRECT_URI = os.environ.get('SPOTIPY_REDIRECT_URI')
auth_token = request.args['code']
code_payload = {
"grant_type": "authorization_code",
"code": auth_token,
"redirect_uri": REDIRECT_URI,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload)
# Auth Step 5: Tokens are Returned to Application
response_data = json.loads(post_request.text)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
# At this point, there is to generate a custom token for the frontend
# Either a self-contained signed JWT or a random token?
# In case the token is not a JWT, it should be stored in the session (in case of a stateful API)
# or in the database (in case of a stateless API)
# In case of a JWT, the authenticity can be tested by the backend with the signature so it doesn't need to be stored at all?
res = make_response(redirect('http://localhost/about', code=302))
return res
Note: this a possible endpoint for getting new Spotify tokens:
@spotify_auth_bp.route("/refresh_token", methods=['GET', 'POST'])
def refresh_token():
SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token"
CLIENT_ID = os.environ.get('SPOTIPY_CLIENT_ID')
CLIENT_SECRET = os.environ.get('SPOTIPY_CLIENT_SECRET')
code_payload = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
}
encode = 'application/x-www-form-urlencoded'
auth = base64.b64encode("{}:{}".format(CLIENT_ID, CLIENT_SECRET).encode())
headers = {"Content-Type" : encode, "Authorization" : "Basic {}".format(auth)}
post_request = requests.post(SPOTIFY_TOKEN_URL, data=code_payload, headers=headers)
response_data = json.loads(post_request.text)
access_token = response_data["access_token"]
refresh_token = response_data["refresh_token"]
token_type = response_data["token_type"]
expires_in = response_data["expires_in"]
return access_token
What is the best way of handling my tokens after Spotify callback?
Considering that, once user is logged with the app, he will also be logged with Spotify non-stop, having to refresh Spotify's access token every 60 minutes:
Is Authorization Code a server-to-server flow only to protect secret app credentials, and then it is safe to have tokens at frontend?
Should I keep both Access token and refresh tokens stored at frontend, and have a Stateless JWT?
Should I keep only temporary access token and keep refresh tokens at database, having a Stateful JWT?
Should I opt for a Session, persisted only server-side, instead?
What is the safest way of handling my sensitive data here? And, considering the code above, how so?
A huge number of questions here! Let's take them one by one:
Is Authorization Code a server-to-server flow only to protect secret app credentials, and then it is safe to have tokens at frontend?
In the Authorization Code
grant, you have to exchange the Authorization Code
for a token. This is done with a request to /token
(grant_type
: authorization_code
) and it requires your client_id
and client_secret
which is secretly stored in your server (aka not-public in your react web app). In this context it's indeed server-to-server.
Should I keep both Access token and refresh tokens stored at frontend, and have a Stateless JWT?
In your case, I would say no. If the token will be used to do some API request to Spotify on server-side, please keep access_token
and refresh_token
server-side.
But then, it's not anymore stateless ? Indeed.
If you really want/need stateless tokens, IMHO you could store the access_token
in a Cookie with following options (and it's mandatory):
PRO:
CON:
refresh_token
.I would recommend to store refresh tokens server-side because it's usually a long-life token.
access_token
expire ?When a request comes with an expired access_token
, you can simply refresh the access_token
with server-side-stored refresh_token
, do the job, and return the response with a new access_token
stored through Set-Cookie
header.
If you always have JWT and you store them in Http-Only cookies, you'll probably say that you don't have any way to know if your are logged-in from your React app. Well there is a trick I already experimented with JWT which is pretty nice.
A JWT is composed of 3 parts; the header, the payload and the signature. What you actually want to protect in your cookies is the signature. Indeed, if you don't have the right signature the JWT is useless. So what you could do is to split the JWT and make only the signature Http-Only.
In your case it should look like:
@app.route('/callback')
def callback():
# (...)
access_token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJSYXBoYWVsIE1lZGFlciJ9.V5exVQ92sZRwRxKeOFxqb4DzWaMTnKu-VmhW-r1pg8E'
a11n_h, a11n_d, a11n_s = access_token.split('.')
response = redirect('http://localhost/about', 302)
response.set_cookie('a11n.h', a11n_h, secure=True)
response.set_cookie('a11n.d', a11n_d, secure=True)
response.set_cookie('a11n.s', a11n_s, secure=True, httponly=True)
return response
You would have 3 cookies:
a11n.h
: the header (options: Secure)a11n.d
: the payload (options: Secure)a11n.s
: the signature (options: Secure, Http-Only)The consequence is:
a11n.d
cookie is accessible from your React app (you can even get userinfo from it)a11n.s
cookie is not accessible from Javascriptaccess_token
from cookies on server-side before sending request to SpotifyTo reassemble the access_token
:
@app.route('/resource')
def resource():
a11n_h = request.cookies.get('a11n.h')
a11n_d = request.cookies.get('a11n.d')
a11n_s = request.cookies.get('a11n.s')
access_token = a11n_h + '.' + a11n_d + '.' + a11n_s
jwt.decode(access_token, verify=True)
I hope it helps!
Disclaimer:
Code samples need to be improved (error handling, checks, etc). They are only examples to illustrate the flow.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With