mblackgeo / flask-cognito-lib

A Flask extension that supports protecting routes with AWS Cognito following OAuth 2.1 best practices
https://mblackgeo.github.io/flask-cognito-lib/
MIT License
57 stars 15 forks source link

error in redirect url in login function #21

Closed aeneasx closed 3 weeks ago

aeneasx commented 1 year ago

for a given aws_cognito_domain the login function returns an error in the html string: https://example.auth.eu-central-1.amazoncognito.com/error?error=redirect_mismatch&client_id=some_id

mblackgeo commented 1 year ago

Can you provide some more information on how you have configured Cognito and how your routes in Flask are setup?

aeneasx commented 1 year ago

In the mean time i found out, why the url was faulty. In my case, when the parts of the url e.g. the scope were in the wrong order it had thrown an error. So the information passed for the aws service with the variable full_url had to be in the first place (auth_url then client_id, response_type, redirect_uri) and afterwards i was able to append the state and the code_challenge etc. to not get an error message. Additionally the host UI could not load correctly if after the aws_cognito_domain was not login? instead of /oauth2/authorize from the authorize_endpoint function in the config file.

E.g. https://auth.example.com/login?client_id=some_id&response_type=code&scope=email+openid&redirect_uri&state=somestring&nonce=somestring&codechallenge=somestring

I configured Cognito with required MFA, required attributes and a custom domain. As i got confused, i tried the example you provided in your repo, but was not able to get it work correctly.

What i did quickly to get it work:

config.py

@property
def authorize_endpoint(self) -> str:
"""Return the Cognito AUTHORIZE endpoint URL"""
    return f"{self.domain}/login"

cognito_svc.py

full_url = (
    f"{self.cfg.authorize_endpoint}"
    f"?client_id={self.cfg.user_pool_client_id}"
    f"&response_type=code"
    f"&redirect_uri={quoted_redirect_url}")

if scopes is not None:
    full_url += f"&scope={'+'.join(scopes)}"

full_url= (
   f"{full_url}&state={state}"
   f"&nonce={nonce}"
   f"&code_challenge={code_challenge}"
    f"&code_challenge_method=S256")

return full_url

Thanks already :)

mblackgeo commented 1 year ago

According to the AWS Cognito docs, the oauth2/authorize endpoint provides a redirect to either the identity provider or the login endpoint and silently passes through the query parameters. As far as I can tell, this shouldn't be causing any problems, do you have any more logs about what was failing?

The order of the query parameters should not make any difference and it looks like all of the mandatory ones are being populated. I don't have access to a Cognito user pool for testing at the moment so I would be grateful for anymore information you can provide, thanks

lizard777 commented 7 months ago

also came across the same issue with local development. Having a bit of a problem debugging because there are no obvious indications of what could be wrong. After testing and a bit of debugging, there seems to be no communication to get the session token, and throws an error with code_verifier. Could definetly use some help to figure out what could be done to resolve this issue

mblackgeo commented 7 months ago

Hi, please could you provide more details about how you have configured Cognito, and what your flask routes look like? I would suggest to have a look at the complete example application where you can see both the routes in Flask and the CDK code that configures AWS Cognito

liz-procogia commented 7 months ago

Hi there, i configured the cognito credentials as stated in the example application, in a file called .env.example.

The file info looks like this AWS_REGION= "us-east-2" AWS_COGNITO_DOMAIN="https://domain.auth.us-east-2.amazoncognito.com" AWS_COGNITO_USER_POOL_ID="insert cognito user pool id" AWS_COGNITO_USER_POOL_CLIENT_ID="insert cognito pool client id" AWS_COGNITO_USER_POOL_CLIENT_SECRET="insert cognito user pool client secret" AWS_COGNITO_REDIRECT_URL="https://localhost:5000/postlogin" AWS_COGNITO_LOGOUT_URL="https://localhost:5000/postlogout" AWS_COGNITO_COOKIE_AGE_SECONDS=1800 BASE_URL="https://localhost:5000"

Both the redirect url. and logout url are on my app integration cognito , under callback urls. I am able to get the redirect to the login page of cognito, however when testing, once the user logs in, the user is redirected to a 500 error page. My plan is that the cognito redirects to a custom html template which is protected. I am testing on local host, however enabled SSL, so my links that are associated to my flask app are https and not http.

liz-procogia commented 7 months ago

my app.py looks like this

from os import path, environ, urandom
from dotenv import load_dotenv
from flask import Flask,redirect, url_for, request,render_template,abort, flash, send_from_directory, jsonify, send_file,session
import json
import pandas as pd
import numpy as np
import datetime
import requests

from flask_cognito_lib import CognitoAuth
from flask_cognito_lib.decorators import(
    auth_required, 
    cognito_login, 
    cognito_login_callback,
    cognito_logout,
)
from flask_cognito_lib.exceptions import (
    AuthorisationRequiredError, 
    CognitoGroupRequiredError,
)

class Config:
    #Set Flask configuration vars from .env file.

    # General Config
    SECRET_KEY = environ.get("SECRET_KEY", urandom(32))
    FLASK_APP = "TEST_APP"
    FLASK_ENV = "TESTING"

    # Cognito config
    # AWS_COGNITO_DISABLED = True  # Can set to turn off auth (e.g. for local testing)
    AWS_REGION = environ["AWS_REGION"]
    AWS_COGNITO_USER_POOL_ID = environ["AWS_COGNITO_USER_POOL_ID"]
    AWS_COGNITO_DOMAIN = environ["AWS_COGNITO_DOMAIN"]
    AWS_COGNITO_USER_POOL_CLIENT_ID = environ["AWS_COGNITO_USER_POOL_CLIENT_ID"]
    AWS_COGNITO_USER_POOL_CLIENT_SECRET = environ["AWS_COGNITO_USER_POOL_CLIENT_SECRET"]
    AWS_COGNITO_REDIRECT_URL = environ["AWS_COGNITO_REDIRECT_URL"]
    AWS_COGNITO_LOGOUT_URL = environ["AWS_COGNITO_LOGOUT_URL"]
    AWS_COGNITO_COOKIE_AGE_SECONDS = environ["AWS_COGNITO_COOKIE_AGE_SECONDS"]

app = Flask(__name__)
app.config.from_object(Config)
auth = CognitoAuth(app)

# Content Security Policy Header
csp_header = {
    'default-src': ["'self'"],
    'script-src': ["insert https", "insert https"],
    # Add other directives as needed
}

@app.route("/login/")
@cognito_login
def login():
    pass

@app.route("/postlogin", methods =('GET','POST'))
@cognito_login_callback
def postlogin():
    return redirect(url_for("/login_success"))

@app.route("/login_success")
@auth_required()
def login_success():
    return render_template("login_success.html")

@app.route("/func_one/", methods =('GET','POST'))
@auth_required()
def func_one():
   .. *other code* ..
   return render_template("func_one.html")

@app.route("/func_two/", methods =('GET','POST'))
@auth_required()
def func_two():
   .. *other code* ..
        return render_template("func_two.html")

@app.route("/func_three/", methods = ['POST','GET'])
@auth_required()
def func_three():
 .. *other code* ..
        return render_template("func_three.html")

@app.route("/func_four/", methods = ['POST','GET'])
@auth_required()
def func_four():
 .. *other code* ..
        return render_template("func_four.html")

@app.route("/func_five/", methods = ['POST','GET'])
@auth_required()
def func_five():
 .. *other code* ..
        return render_template("func_five.html")

@app.route("/func_six/", methods = ['GET','POST'])
@auth_required()
def func_six():
 .. *other code* ..
        return render_template("func_six.html")

@app.route("/func_seven/", methods = ['GET','POST'])
@auth_required()
def func_seven():
 .. *other code* ..
        return render_template("func_seven.html")

@app.route("/logout")
@cognito_logout
def logout(): 
   pass

@app.route("/postlogout")
def postlogout():
    return render_template('login.html')

@app.route("/messages/<int:idx>")
def message(idx):

    messages = ['Message Zero', 'Message One', 'Message Two']
    try:

        return render_template('message.html', message=messages[idx])
    except IndexError:

        abort(404)

@app.errorhandler(404)
def page_not_found(error):
    return render_template("404.html"),404

@app.route("/500/")
def error500():
    abort(500)

@app.errorhandler(500)
def internal_error(error):
    return render_template('500.html'),500

@app.after_request
def apply_csp(response):
    response.headers['Content-Security-Policy'] = '; '.join([f"{key} {' '.join(value)}" for key, value in csp_header.items()])
    return response

@app.errorhandler(AuthorisationRequiredError)
def auth_error_handler(err):
    return redirect(url_for("login"))

@app.errorhandler(CognitoGroupRequiredError)
def missing_group_error_handler(err):
    return jsonify("Group memberhship does not allow acces to this resource"),403

if __name__ == "__main__":
    app.secret_key ='super secret key'
    app.debug = True
    app.run(ssl_context=('somefile.pem','somefile.pem'))
apantovic commented 3 months ago

I've run into same error while trying to setup and test login functionality with cognito, the problem for me was having http instead of https in cognito redirect.

I'm facing new problem afterwards, since after login I'm getting the error:

flask_cognito_lib.exceptions.CognitoError: Error getting public keys from Cognito]

and I'm not sure how to handle this one, is there additional setting or some param to setup?

mblackgeo commented 3 months ago

I haven't been able to replicate this one, but then again I don't have easy access to Cognito at the moment.

To validate the signature, we use get_signing_key_from_jwt() for PyJWT. From the Cognito docs:

The JWKS URI contains public information about the private key that signed your user's token. You can find the JWKS URI for your user pool at https://cognito-idp.<Region>.amazonaws.com/<userPoolId>/.well-known/jwks.json.

So I guess first thing to verify is if you can connect and obtain the JWKs from that endpoint