summaryrefslogtreecommitdiffstats
path: root/aurweb/routers/sso.py
blob: e1ec7efedc776ee07eec2b764ba4a4def79aeffb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import time
import uuid

import fastapi

from authlib.integrations.starlette_client import OAuth
from fastapi import Depends, HTTPException
from fastapi.responses import RedirectResponse
from sqlalchemy.sql import select
from starlette.requests import Request

import aurweb.config
import aurweb.db

from aurweb.schema import Sessions, Users

router = fastapi.APIRouter()

oauth = OAuth()
oauth.register(
    name="sso",
    server_metadata_url=aurweb.config.get("sso", "openid_configuration"),
    client_kwargs={"scope": "openid"},
    client_id=aurweb.config.get("sso", "client_id"),
    client_secret=aurweb.config.get("sso", "client_secret"),
)


@router.get("/sso/login")
async def login(request: Request):
    """
    Redirect the user to the SSO provider’s login page.

    We specify prompt=login to force the user to input their credentials even
    if they’re already logged on the SSO. This is less practical, but given AUR
    has the potential to impact many users, better safe than sorry.
    """
    redirect_uri = aurweb.config.get("options", "aur_location") + "/sso/authenticate"
    return await oauth.sso.authorize_redirect(request, redirect_uri, prompt="login")


def open_session(conn, user_id):
    """
    Create a new user session into the database. Return its SID.
    """
    # TODO check for account suspension
    # TODO apply [options] max_sessions_per_user
    sid = uuid.uuid4().hex
    conn.execute(Sessions.insert().values(
        UsersID=user_id,
        SessionID=sid,
        LastUpdateTS=time.time(),
    ))
    # TODO update Users.LastLogin and Users.LastLoginIPAddress
    return sid


@router.get("/sso/authenticate")
async def authenticate(request: Request, conn=Depends(aurweb.db.connect)):
    """
    Receive an OpenID Connect ID token, validate it, then process it to create
    an new AUR session.
    """
    # TODO check for banned IPs
    token = await oauth.sso.authorize_access_token(request)
    user = await oauth.sso.parse_id_token(request, token)
    sub = user.get("sub")  # this is the SSO account ID in JWT terminology
    if not sub:
        raise HTTPException(status_code=400, detail="JWT is missing its `sub` field.")

    aur_accounts = conn.execute(select([Users.c.ID]).where(Users.c.SSOAccountID == sub)) \
                       .fetchall()
    if not aur_accounts:
        return "Sorry, we don’t seem to know you Sir " + sub
    elif len(aur_accounts) == 1:
        sid = open_session(conn, aur_accounts[0][Users.c.ID])
        response = RedirectResponse("/")
        # TODO redirect to the referrer
        response.set_cookie(key="AURSID", value=sid, httponly=True,
                            secure=request.url.scheme == "https")
        return response
    else:
        # We’ve got a severe integrity violation.
        raise Exception("Multiple accounts found for SSO account " + sub)