Microsoft Authentication - Python Flask msal Example App Ported to FastAPI - python

I don't do much web work but I recently began using FastAPI and am building an MVC app with jinja2 templating that uses PowerBI embedded capacity to serve multiple embedded analytics in app owns data arrangement. All of this works beautifully. However, I'm wanting to add further modules and I'd like to use the msal package to do user authentication by routing a user to the Microsoft login page, letting them sign in against a multi-tenant app service I set up in Azure, and then redirecting back to my page via redirect URI, grabbing the token, and progressing with authorization. Microsoft saved a great example our here for doing this in Flask. However, I am having fits porting the example to FastAPI.
I can get the user to the login screen and log in but I am having no luck capturing the token at my call back URI - it's appropriately routing but I am unable to capture the token from the response.
Has anyone (or can anyone) taken that super simple Flask example and ported it to FastAPI? Everything I find online for FAPI is back-end token-bearer headers for APIs - not meant for MVC apps.
Here's my current code. Messy because I have "tests" built in.
import msal
import requests
from fastapi import APIRouter, Request, Response
from fastapi.responses import RedirectResponse
from starlette.templating import Jinja2Templates
from config import get_settings
settings = get_settings()
router = APIRouter()
templates = Jinja2Templates('templates')
# Works
#router.get('/login', include_in_schema=False)
async def login(request: Request):
request.session['flow'] = _build_auth_code_flow(scopes=settings.AUTH_SCOPE)
login_url = request.session['flow']['auth_uri']
return templates.TemplateResponse('error.html', {'request': request, 'message': login_url})
# DOES NOT WORK - Pretty sure error is in here --------------------
#router.get('/getAToken', response_class=Response, include_in_schema=False)
async def authorize(request: Request):
try:
cache = _load_cache(request)
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
request.session.get('flow'), request.session
)
if 'error' in result:
return templates.TemplateResponse('error.html', {'request': request, 'message': result})
request.session['user'] = result.get('id_token_claims')
_save_cache(cache)
except Exception as error:
return templates.TemplateResponse('error.html', {'request': request, 'message': f'{error}: {str(request.query_params)}'})
return templates.TemplateResponse('error.html', {'request': request, 'message': result})
# -----------------------------------------------------
def _load_cache(request: Request):
cache = msal.SerializableTokenCache()
if request.session.get("token_cache"):
cache.deserialize(request.session["token_cache"])
return cache
def _save_cache(request: Request, cache):
if cache.has_state_changed:
request.session["token_cache"] = cache.serialize()
def _build_msal_app(cache=None, authority=None):
return msal.ConfidentialClientApplication(
settings.CLIENT_ID,
authority=authority or settings.AUTH_AUTHORITY,
client_credential=settings.CLIENT_SECRET,
token_cache=cache
)
def _build_auth_code_flow(authority=None, scopes=None):
return _build_msal_app(authority=authority).initiate_auth_code_flow(
scopes or [],
redirect_uri=settings.AUTH_REDIRECT)
def _get_token_from_cache(scope=None):
cache = _load_cache() # This web app maintains one cache per session
cca = _build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(scope, account=accounts[0])
_save_cache(cache)
return result
Any help is GREATLY appreciated. Happy to answer any questions. Thank you.

This is because FastAPI session variables are stored client-side as a cookie, which has a limit of 4096 bytes of data. The data being stored from the redirect url is pushes the cookie size over this limit and results in the data not being stored. Starlette-session is an alternative SessionMiddleware that stores variables server-side, eliminating cookie limit. Below is a basic (but messy) implementation:
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from starlette.requests import Request
from starlette.responses import RedirectResponse
from starlette_session import SessionMiddleware
from starlette_session.backends import BackendType
from redis import Redis
import uvicorn
import functools
import msal
app_client_id = "sample_msal_client_id"
app_client_secret = "sample_msal_client_secret"
tenant_id = "sample_msal_tenant_id"
app = FastAPI()
redis_client = Redis(host="localhost", port=6379)
app.add_middleware(
SessionMiddleware,
secret_key="SECURE_SECRET_KEY",
cookie_name="auth_cookie",
backend_type=BackendType.redis,
backend_client=redis_client,
)
templates = Jinja2Templates(directory="templates")
default_scope = ["https://graph.microsoft.com/.default"]
token_cache_key = "token_cache"
# Private Functions - Start
def _load_cache(session):
cache = msal.SerializableTokenCache()
if session.get(token_cache_key):
cache.deserialize(session[token_cache_key])
return cache
def _save_cache(cache,session):
if cache.has_state_changed:
session[token_cache_key] = cache.serialize()
def _build_msal_app(cache=None):
return msal.ConfidentialClientApplication(
app_client_id,
client_credential=app_client_secret,
authority=f"https://login.microsoftonline.com/{tenant_id}",
token_cache=cache
)
def _build_auth_code_flow(request):
return _build_msal_app().initiate_auth_code_flow(
default_scope, #Scopes
redirect_uri=request.url_for("callback") #Redirect URI
)
def _get_token_from_cache(session):
cache = _load_cache(session) # This web app maintains one cache per session
cca = _build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(default_scope, account=accounts[0])
_save_cache(cache,session)
return result
# Private Functions - End
# Custom Decorators - Start
def authenticated_endpoint(func):
#functools.wraps(func)
def is_authenticated(*args,**kwargs):
try:
request = kwargs["request"]
token = _get_token_from_cache(request.session)
if not token:
return RedirectResponse(request.url_for("login"))
return func(*args,**kwargs)
except:
return RedirectResponse(request.url_for("login"))
return is_authenticated
# Custom Decorators - End
# Endpoints - Start
#app.get("/")
#authenticated_endpoint
def index(request:Request):
return {
"result": "good"
}
#app.get("/login")
def login(request:Request):
return templates.TemplateResponse("login.html",{
"version": msal.__version__,
'request': request,
"config": {
"B2C_RESET_PASSWORD_AUTHORITY": False
}
})
#app.get("/oauth/redirect")
def get_redirect_url(request:Request):
request.session["flow"] = _build_auth_code_flow(request)
return RedirectResponse(request.session["flow"]["auth_uri"])
#app.get("/callback")
async def callback(request:Request):
cache = _load_cache(request.session)
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(request.session.get("flow", {}), dict(request.query_params))
if "error" in result:
return templates.TemplateResponse("auth_error.html",{
"result": result,
'request': request
})
request.session["user"] = result.get("id_token_claims")
request.session[token_cache_key] = cache.serialize()
return RedirectResponse(request.url_for("index"))
# Endpoints - End
if __name__ == "__main__":
uvicorn.run("main:app",host='0.0.0.0', port=4557,reload=True)`

Related

How to use #token_required decorator for PyJWT in Flask App to access route after user gets logged in?

I've used PyJWT for authenticating the user now my main concern is how to use authentication decorator in API endpoints as I've addes SQL query to to fetch user detail using uid in my route but in token_required definition for current user do I've to add that query again?
Ex. After login I want to access API to display user profile.
#app.route('/users/<uid>', methods=['GET'])
**#token_required** ??
I've used SqlAlchemy core to execute and get data from database in my route.
In token_required definition can we add SqlAlchmey core query for current user & how to implement because I've already used that in my route /users/.
def token_required(f):
#wraps(f)
def decorator(*args, **kwargs):
token = None
if 'x-access-tokens' in request.headers:
token = request.headers['x-access-tokens']
if not token:
return jsonify({'message': 'a valid token is missing'})
try:
data = jwt.decode(token, app.config['SECRET_KEY'])
current_user = User.query.filter_by(uid=data['uid']).first()
except:
return jsonify({'message': 'token is invalid'})
return f(current_user, *args, **kwargs)
return decorator
#app.route('/users/<uid>', methods=['GET'])
def profile_view(uid):
print("user_details")
conn = engine.connect()
str_sql = text(**"""SELECT * FROM user WHERE uid = uid""",{"uid": uid}**)
results = conn.execute(str_sql).fetchall()
print(results)
return users_scehma.dump(results)
First of all import flask and JWTManager
from flask import Flask, jsonify
from flask_jwt_extended import (create_access_token,get_jwt_identity,jwt_required,JWTManager)
Define the Flask app and set a JWT_SECRET_KEY
app = Flask(__name__)
app.config["JWT_SECRET_KEY"] = "supersecret" # Change this!
Initialize the jwt with the app
jwt = JWTManager(app)
Make a #app.route to define the method and path.
def login() is to get the Authentifikation key
#app.route("/token/login", methods=["GET"])
def login():
additional_claims = {"aud": "some_audience", "foo": "bar"}
access_token = create_access_token(identity="username", additional_claims=additional_claims) #you can add additional parameters
return jsonify(access_token=access_token),200#, encoded=False), 200
Our second route is protected to with #jwt_required()
#app.route("/protected", methods=["GET"])
#jwt_required()
def protected():
# Access the identity of the current user with get_jwt_identity
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
if __name__ == '__main__':
app.run()
the code looks like this at the end:
from flask import Flask, jsonify
from flask_jwt_extended import (create_access_token,get_jwt_identity,jwt_required,JWTManager)
app = Flask(__name__)
app.config["JWT_SECRET_KEY"] = "supersecret" # Change this!
jwt = JWTManager(app)
#app.route("/token/login", methods=["Get"])
def login():
additional_claims = {"aud": "some_audience", "foo": "bar"}
access_token = create_access_token(identity="username", additional_claims=additional_claims) #you can add additional parameters
return jsonify(access_token=access_token),200#, encoded=False), 200
#app.route("/protected", methods=["GET"])
#jwt_required()
def protected():
# Access the identity of the current user with get_jwt_identity
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
if __name__ == '__main__':
app.run()
Output:
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2Njk2MTA3MCwianRpIjoiNGViY2MwYzAtMjAxYy00ODAwLThjMTUtNmQzNDQ1MmVhYmQxIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InVzZXJuYW1lIiwibmJmIjoxNjY2OTYxMDcwLCJleHAiOjE2NjY5NjE5NzAsImF1ZCI6InNvbWVfYXVkaWVuY2UiLCJmb28iOiJiYXIifQ.qAn8rhsxyF_00Ayu9L7ddd6USkbYIHKvsUneDMzzjHs"}
To use the access_token we need an module that can call the Webserver with header.
I will use requests
import requests
key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTY2Njk2MTA3MCwianRpIjoiNGViY2MwYzAtMjAxYy00ODAwLThjMTUtNmQzNDQ1MmVhYmQxIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6InVzZXJuYW1lIiwibmJmIjoxNjY2OTYxMDcwLCJleHAiOjE2NjY5NjE5NzAsImF1ZCI6InNvbWVfYXVkaWVuY2UiLCJmb28iOiJiYXIifQ.qAn8rhsxyF_00Ayu9L7ddd6USkbYIHKvsUneDMzzjHs"
requests.get("http://127.0.0.1:5000/protected", headers={'Authorization': 'Bearer ' + key}
If you want to set an expire time set this:
app.config["JWT_SECRET_KEY"] = "supersecret" # Change this!
from datetime import timedelta
pp.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(minutes=1000)
jwt = JWTManager(app)
if you need to know more you can follow the link:
https://flask-jwt-extended.readthedocs.io/en/stable/token_locations/
On the right side is an Navigation to go throw the sides.
This will also show you how to use Cookies with the #jwt_required()

How can the following issue be resolved in flask? "Method Not Allowed The method is not allowed for the requested URL"

Here is the code
import os
import redis
import flask
import json
import urllib.parse
from flask import Flask, Response, request, render_template, abort
from flask_cors import CORS, cross_origin
#from flask.ext.cors import CORS, cross_origin
app = Flask(__name__)
app.config['CORS_HEADERS'] = 'Content-Type'
redis_handle = redis.Redis('localhost')
requiredFields = ("id", "title", "name") # fields required for user object
#app.route('/')
#cross_origin()
def hello():
return 'Hello World!'
#app.route('/users/<user_id>', methods=['GET'])
#cross_origin()
def get_user(user_id):
response = {}
# user_id = request.args.get("id")
user = redis_handle.get(user_id)
if not user:
response["msg"] = "no user found"
return Response(json.dumps(response), status=404, mimetype="application/json")
return user
#app.route('/users', methods=['POST'])
#cross_origin()
def save_user():
data = request.get_json(force=True)
response = {}
if all(field in data for field in requiredFields):
redis_handle.set(data["id"], json.dumps(data))
return Response(status=201)
else:
missing_key = str([val for val in requiredFields if val not in dict(data).keys()])
response["msg"] = "required key " + missing_key + " not found"
return Response(json.dumps(response), status=400)
#app.route('/users/<user_id>', methods=['DELETE'])
#cross_origin()
def delete_user(user_id):
response = {}
resp = redis_handle.delete(user_id)
if resp == 0:
response["msg"] = "no such entity found"
status = 404
else:
response["msg"] = "Delete op is successful"
status = 200
return Response(json.dumps(response), status=status)
#app.route('/clear', methods=['GET'])
#cross_origin()
def clear_data():
redis_handle.flushall()
return "ok!"
if __name__ == "__main__":
app.run(debug=True)
As of my knowledge, I have even included the method = "POST" as well but still don't know what is going wrong.
I tried to create a small crud application using redis, python, flask but couldn't encountering this issue. Can someone tell me where and what am I doing wrong?
Browsers don't run POST methods outside of a <form> entry or AJAX function. Therefore, you're running a GET, which "isn't allowed".
Unclear what you expected, but to see all users, you'll need to edit your route to first add the GET method, then if so, return a response that returns/renders all users rather than checking the request json body, which won't exist for GET requests
If you only wanted to get one user, edit the url to include the user ID
The browser will use the GET method for URLs that you input in URL/search bar but you don't have any function decorated with #app.route('/users', methods=['GET']).
If you want to create a user with POST /users then it would be easier to use some HTTP client like https://www.postman.com, https://insomnia.rest, etc. or even fetch in the browser's console.

Authentication verification in Python based GraphQL Server using FastAPI

I am having trouble implementing authentication verification in an GraphQL Server built with FastAPI. Before, we were using REST but now we are switching to GraphQL and I am wondering how I can implement this. Before, we had different routers and with FastAPI it is easy to check authentication based on routes using dependencies as in here. We are sending a Token in the Authorization Header which we are decoding in the backend and getting back the user_id which we can then use in our different endpoints.
I am wondering how this might work using GraphQL here. We use the Graphene and I had a look at Starlettes Authentication Examples as well as there intro into setting up GraphQl
import binascii
from fastapi import FastAPI
from starlette.authentication import (
AuthenticationBackend, AuthenticationError, SimpleUser, AuthCredentials
)
from starlette.graphql import GraphQLApp
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from schemas.root import my_schema
class BasicAuthBackend(AuthenticationBackend):
async def authenticate(self, request):
if "Authorization" not in request.headers:
raise AuthenticationError('No auth credentials')
auth = request.headers["Authorization"]
try:
id_token = auth.split('Bearer ')[1]
decoded_token = auth.verify_id_token(id_token)
except (ValueError, UnicodeDecodeError, binascii.Error) as exc:
raise AuthenticationError('Invalid basic auth credentials')
user_id = decoded_token['uid']
return AuthCredentials(["authenticated"]), user_id
middleware = [
Middleware(AuthenticationMiddleware, backend=BasicAuthBackend())
]
my_schema = Schema(
query=RootQuery,
mutation=RootMutation,
)
api = FastAPI(title=f"MyGraphQLServer", middleware=middleware)
api.add_route("/graphql", GraphQLApp(schema=my_schema))
For example, imagine that I now would only like to authenticate mutation Requests but not query requests. Furthermore I want to access the user_id in each of my resolvers. What would be the best way to do this?
In FastAPI documentation or starlette documentation they use add_route, that is the way to add a route in Starlette without declaring the specific operation (as would be with .get(), .post(), etc). But it has some disadvantages, we can't add dependencies like we do in FastAPI, example below
app.add_route(
"/graphql",
GraphQLApp(schema=graphene.Schema(query=Query),
executor_class=AsyncioExecutor),
dependencies=(Depends(SomeAuthorizationStuffHere)),
)
So we need to do in FastAPI, i created a simple app with HTTPBasicAuth, you can expand this with other Method's you just need to include the router(s)
from fastapi import Query, Depends, Request, FastAPI, APIRouter
from fastapi.security import HTTPBasic, HTTPBasicCredentials
import graphene
from graphene import Field, Schema, String, ObjectType
from starlette.graphql import GraphQLApp
router = APIRouter()
app = FastAPI()
security = HTTPBasic()
class Query(ObjectType):
hello = Field(String, name=String())
def resolve_hello(root, info, name):
coverage = info.context["request"].state.some_dep
return f"Hello {some_dep.some_method(name)}"
graphql_app = GraphQLApp(schema=Schema(query=Query))
#router.api_route("/gql", methods=["GET", "POST"])
async def graphql(request: Request):
return await graphql_app.handle_graphql(request=request)
app.include_router(router, dependencies=[Depends(security)])

Google Functions Exceeding Memory

We have deployed a python function on GCP Functions that basically receives a HTTP POST JSON, and calls for another system to do some processing. This other system returns a JSON, and we send the JSON back to the original caller.
The function works great with 128mb of memory, but after we got a new customer, they are making one call after another to the function, and now GCP throws a memory limit exceeded error and breaks the process.
I realize that if the customer waits a few min between the calls, the memory goes back down, but I cant ask them to do it.
My question is, is there a way to prevent this massive memory increase after various sequential calls to the function? Or any alternative to detect its getting to the limit and send back a timeout error?
Part of my code
#!venv/bin/python
from flask import Flask, request, Response,jsonify,make_response
import json
import requests
from functools import wraps
import hashlib
from google.cloud import firestore
import datetime
app = Flask(__name__)
app.secret_key = b'NOTHERE'
def check_auth(username, password):
pwd = hashlib.sha1(str(password).encode('utf-8')).hexdigest()
db = firestore.Client()
users_ref = db.collection('XXX')
doc_ref = users_ref.document(username)
try:
doc = doc_ref.get()
dic = doc.to_dict()
return pwd == dic['password']
except:
pass
return False
def authenticate():
"""Sends a 401 response that enables basic auth"""
return Response(
'Please Login', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def requires_auth(f):
#wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
#main function that google calls
#requires_auth
def brokering(request):
db = firestore.Client()
if request.method == 'POST':
......... here I do some write/reads on the firestore db
Memory graph until it breaks(130 calls)
I would move as much as possible outside the function. For example, you're currently creating two firestore.Client() instances, one in the brokering and one in check_auth, for every single request. You should move this outside your function definitions, and reuse a single client instance inside the functions.
You're also doing some unnecessary stuff, like initializing an entire Flask app that is unused by Cloud Functions, but cuts into your memory overhead.
I'd rewrite your entire function to be something like this:
from flask import request, Response
from functools import wraps
import hashlib
from google.cloud import firestore
db = firestore.Client()
users_ref = db.collection('XXX')
def check_auth(username, password):
pwd = hashlib.sha1(str(password).encode('utf-8')).hexdigest()
doc_ref = users_ref.document(username)
try:
return pwd == doc_ref.get().to_dict()['password']
except:
pass
return False
def authenticate():
"""Sends a 401 response that enables basic auth"""
return Response(
'Please Login', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'})
def requires_auth(f):
#wraps(f)
def decorated(*args, **kwargs):
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
#requires_auth
def brokering(request):
if request.method == 'POST':
... # here I do some write/reads on the firestore db

Python Pyramid AuthTktAuthenticationPolicy auto-expired cookies based on inactivity issue

Someone please help. I'm new to pyramid. So according to the pyramid docs about AuthTktAuthenticationPolicy
developers says that we can set up auto-expired cookies based on inactivity - awesome.
But it doesn't work for me, so it says that you need to use reissue_time parameter and set it in pair with timeout. If timeout is for example set to 1200, so then reissue_time should be timout / 10 = 120, 2 minutes.
As i understood the point is to make auto logout after 2 minutes when a user is inactive. But when i try to reload the page token or session is not expiring.
#myapp/__init__.py
from pyramid.config import Configurator
from sqlalchemy import engine_from_config
from myapp.models import initialize_sql
from myapp import views
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
authentication_policy = AuthTktAuthenticationPolicy(secret='secret', hashalg='sha512', timeout=1200, reissue_time=120)
authorization_policy = ACLAuthorizationPolicy()
config = Configurator(settings=settings, root_factory='myapp.factory.RootFactory', )
config.include('pyramid_jinja2')
#config.add_jinja2_renderer('.html', settings_prefix='jinja2.')
config.add_static_view('static', 'static', cache_max_age=3600)
config.scan('myapp.models')
config.set_authentication_policy(authentication_policy)
config.set_authorization_policy(authorization_policy)
engine = engine_from_config(settings, 'sqlalchemy.')
initialize_sql(engine)
views.routes(config)
return config.make_wsgi_app()
#factory/rootfactory.py
from pyramid.security import (
Allow,
Authenticated,
)
class RootFactory(object):
'''Root factory class. Acl auth system'''
__acl__ = [(Allow, Authenticated, 'view'),]
def __init__(self, request):
pass
#views/__init__.py
def routes(config):
config.add_route('home', '/')
config.add_route('login', '/login')
config.add_route('logout', '/logout')
config.scan('myapp')
#views/auth.py
from pyramid.response import Response
from pyramid.security import remember, forget, authenticated_userid
from pyramid.httpexceptions import HTTPFound, HTTPForbidden
from pyramid.view import view_config, forbidden_view_config
from sqlalchemy.exc import DBAPIError
from ..models import DBSession
from ..models.user import User
from ..forms.login import LoginForm
#view_config(route_name='login', renderer='myapp:templates/login.jinja2')
#forbidden_view_config(renderer='myapp:templates/login.jinja2')
def login_view(request):
if request.authenticated_userid:
return HTTPFound(location=request.application_url)
next = request.params.get('next') or request.route_url('home')
login_form = LoginForm(request.POST)
if request.method == 'POST' and login_form.validate():
login = request.params.get('login', '')
password = request.params.get('password', '')
user = User.by_login(login)
if user and user.validate_password(password):
headers = remember(request, login)
return HTTPFound(location=next, headers=headers)
return {'form': login_form}
#view_config(route_name='logout', renderer='myapp:templates/logout.jinja2')
def logout_view(request):
headers = forget(request)
loc = request.route_url('login')
return HTTPFound(location=loc, headers=headers)
#views/home.py
from pyramid.security import authenticated_userid
from pyramid.httpexceptions import HTTPForbidden
from pyramid.response import Response
from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
from ..models import DBSession
from ..models.user import User
#view_config(route_name='home', permission='view', renderer='myapp:templates/base.jinja2')
def home_view(request):
return Response('Ok')
Your session is logged out after 1200 seconds, or 20 minutes. You'll be issued a new cookie at most every 120 seconds, or 2 minutes.
In other words, the reissue_time only limits how often a new token is issued. If a user contacts the server very frequently, it would be costly to keep issuing new tokens. Instead, a new token is only issued if the old token is at least reissue_time seconds old. A new token will be fresh for another timeout seconds, so each re-issue extends the session.
Only if a user contacts the server with a cookie issued more than timeout seconds old is the session no longer valid.

Categories