Verificar um token é conferir a assinatura, fixar o algoritmo e checar as claims, não só decodificar.
Quase toda falha desta trilha mora no mesmo ponto: o servidor decodifica o token em vez de verificar. Decodificar é só ler o payload; verificar é provar que o token é legítimo e ainda vale. Uma verificação correta faz, nesta ordem:
confere a assinatura com o segredo do servidor (sem isso, qualquer payload passa);
fixa o algoritmo aceito (algorithms=["HS256"]), nunca o lê do header (fecha o alg none e a confusão de algoritmo);
exige e checa a expiração (exp), para um token vencido não ser reaproveitado;
valida as claims que importam: o emissor (iss), o destinatário (aud), o papel vindo de fonte confiável.
À esquerda, decode com a verificação da assinatura desligada aceita qualquer token; à direita, verify confere assinatura, fixa o algoritmo e checa exp e iss.
Numa biblioteca madura como a PyJWT, o inseguro e o seguro ficam lado a lado:
# inseguro: lê o payload sem provar nada
jwt.decode(token, options={"verify_signature": False})
# seguro: assinatura, algoritmo fixo, expiração e emissor
jwt.decode(token, SECRET, algorithms=["HS256"],
issuer="auth.galpao.test", options={"require": ["exp"]})
Some o segredo forte do começo e a sessão trocada a cada login, e as peças da defesa estão todas no lugar.