Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Troubles with Session Auth #81

Closed
wmshort opened this issue Sep 5, 2021 · 9 comments
Closed

Troubles with Session Auth #81

wmshort opened this issue Sep 5, 2021 · 9 comments

Comments

@wmshort
Copy link

wmshort commented Sep 5, 2021

I am struggling a bit with Piccolo's authentication systems. I have a FastAPI app and am wrapping it with Starlette's AuthenticationMiddleware, as hinted in the docs, with the joint SessionAuth and SecretTokenAuth providers. The secret token seems to be working alright; my API client won't get results without the correct header-cum-token. However whatever I do on the browser gives me "Auth failed for all backends". I can't get to the login endpoint, though this appears properly configured according to the docs. I tried 'allow unauthenticated' to see if this would loosen up the permissions, but even the FastAPI docs give me this error. Is there any robust example app with SessionAuth to see how everything should be organized?

@sinisaos
Copy link
Member

sinisaos commented Sep 5, 2021

@wmshort Piccolo Admin use SessionAuth. You can also try something like this.

auth_middleware = partial(
    AuthenticationMiddleware,
    backend=SessionsAuthBackend(
        auth_table=BaseUser,
        session_table=SessionsBase,
        admin_only=False,
    ),
)

app = FastAPI(docs_url=None) 

app.mount("/docs/", swagger_ui(schema_url="../openapi.json"))
# login endpoint which which provide login form
app.mount(
    path="/login/",
    app=session_login(
        auth_table=BaseUser,
        session_table=SessionsBase,
        redirect_to="/docs/",
    ),
)

@app.post("/logout/", tags=["Logout"])
async def logout(request: Request):
    response = JSONResponse(
        {
            "message": "You are logged out",
        },
        status_code=200,
    )
    response.delete_cookie("id")
    return response

I hope this helps.

@wmshort
Copy link
Author

wmshort commented Sep 5, 2021

Cool, thanks, I will tinker around with this! Maybe I am being daft, but how, in this example, do you hook up the auth_middleware to app? And are these endpoints the equivalent of what's available from piccolo_api.session_auth.endpoints?

Thanks again for your suggestions!

@dantownsend
Copy link
Member

dantownsend commented Sep 5, 2021

@sinisaos Thanks for sharing that example.

Here's some code from one of my apps which might also be helpful:

import datetime
from fastapi import FastAPI
from piccolo_api.csrf.middleware import CSRFMiddleware
from piccolo_api.openapi.endpoints import swagger_ui
from piccolo_api.session_auth.endpoints import session_login, session_logout
from piccolo_api.session_auth.middleware import SessionsAuthBackend
from starlette.middleware import Middleware
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.routing import Route

app = FastAPI()

app.mount(
    "/login/",
    session_login(),
)

private_app = FastAPI(
    routes=[
        Route("/logout/", session_logout()),
    ],
    middleware=[
        Middleware(
            AuthenticationMiddleware,
            backend=SessionsAuthBackend(
                increase_expiry=datetime.timedelta(minutes=30)
            ),
        ),
        Middleware(CSRFMiddleware, allow_form_param=True),
    ],
    docs_url=None,
    redoc_url=None,
)

# The Swagger docs which come with FastAPI don't support CSRF middleware, so we mount
# a custom one which Piccolo provides (accessible at /private/docs):
private_app.mount("/docs/", swagger_ui(schema_url="/private/openapi.json"))

@private_app.get('/my-secret-endpoint/')
def my_endpoint():
    # This is just a normal FastAPI endpoint, and is protected by Session Auth
    pass

app.mount("/private/", private_app)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app)

You'll notice that there's basically two FastAPI apps - a root one, which contains anything which should be publicly accessible (such as the login endpoint) and a child app, which contains anything which should be protected by Session Auth.

@sinisaos
Copy link
Member

sinisaos commented Sep 5, 2021

@wmshort Yes. You need endpoints from piccolo_api.session_auth.endpoints.

@wmshort
Copy link
Author

wmshort commented Sep 5, 2021

Thanks, both, this really helped! The key was thinking about them as distinct apps and separating out the different endpoints. The only sticky thing remaining is that the session_logout endpoint is giving me a "method not allowed" error, but everything else is running smoothly now.

@sinisaos
Copy link
Member

sinisaos commented Sep 6, 2021

@wmshort This happens because session_logout() uses the HTTP POST method and you access it using GET.
If you try curl curl -X POST --cookie "id=yourcookiesvalue" http://localhost:8000/logout/ (or your request.url) you will see that you Successfully logged out. It's easier for me not to use session_logout() but a logout endpoint that I can access from /docs like in my previous example.

# your imports
from starlette.responses import JSONResponse

app = FastAPI(docs_url=None) 

app.mount("/docs/", swagger_ui(schema_url="../openapi.json"))

@app.post("/logout/", tags=["Logout"])
async def logout(request: Request):
    response = JSONResponse(
        {
            "message": "You are logged out",
        },
        status_code=200,
    )
    response.delete_cookie("id") # or response.delete_cookie("yourcookie")
    return response

@dantownsend
Copy link
Member

Yeah, it only supports POST. I had a look at the docs, and this isn't mentioned, so needs adding.

@dantownsend
Copy link
Member

dantownsend commented Sep 6, 2021

If you've got a HTML template, you can put something like this in to logout:

<form action="/private/logout/" method="POST">
    <!--
    You need to get the CSRF token. The CSRF middleware adds it to the FastAPI / Starlette request,
     so if you add the request to the Jinja context, you can access the CSRF token from the template.
     Alternatively you can get the csrftoken from the cookie using jQuery or something.
     -->
    <input name="csrftoken" type="hidden" value="{{ request.scope.get('csrftoken') }}" />
    <button>Logout</button>
</form>

An example of adding the request to the Jinja context:

async def my_endpoint(self, request: Request):
    template = ENVIRONMENT.get_template("app.html.jinja")
    content = template.render(request=request)
    return HTMLResponse(content)

@wmshort
Copy link
Author

wmshort commented Sep 7, 2021

Closing issue, as the last remaining question appears to be addressed by #82

@wmshort wmshort closed this as completed Sep 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants