CAS Authentisierung via Portal

Das CAS-Protokoll ist ein Single-Sign-On Protokoll, das es erlaubt, sich via Portal an den GEVER-Mandanten zu authentisieren.

Authentisierungs-Flow

Der Prozess umfasst vier Schritte:

  1. Authentisieren am Portal mit Benutzername und Passwort
  2. Beziehen eines CAS-Tickets vom Portal
  3. Einlösen des CAS-Tickets gegen ein kurzlebiges Token (JWT) beim Service (GEVER-Mandant)
  4. Verwenden des Tokens um die folgenden Requests an den Service zu authentisieren

Authentisieren am Portal

Um ein CAS-Ticket zu beziehen, muss sich ein Client zuerst am Portal anmelden. Dies geschieht über den /api/login Endpoint auf dem Portal:

Login-Request:

POST /portal/api/login HTTP/1.1
Accept: application/json

{
    "username": "john.doe",
    "password": "secret"
}

Login-Response:

HTTP/1.1 200 OK
Content-Type: application/json
Set-Cookie: csrftoken=...; sessionid=...

{"username":"john.doe","state":"logged_in","invitation_callback":""}

Der Client ist danach über ein Session-Cookie am Portal angemeldet. Der HTTP-Client muss dementsprechend vom Server erhaltene Cookies bei folgenden Requests auch wieder mitschicken.

Zusätzlich zum Session-Cookie sendet das Portal auch ein CSRF-Token als Cookie - dieses muss vom Client ausgelesen, und in folgenden Requests an das Portal im X-CSRFToken HTTP Header mitgeschickt werden.

Der Client muss in folgenden Requests an das Portal auch den Referer HTTP Header setzen (auf die Portal-URL), sonst wird der Request vom CSRF-Protection Mechanismus abgelehnt.

CAS-Ticket vom Portal beziehen

Der Client kann vom Portal über den /api/cas/tickets Endpoint nun ein CAS-Ticket für den gewünschten Service beziehen:

Ticket-Request:

POST /portal/api/cas/tickets HTTP/1.1
Accept: application/json
Referer: https://apitest.onegovgever.ch/portal
X-CSRFToken: ypI3LgB7n7HYKMEd64KjHl3EXEye2XTN4p41AFeG9cCkwGv0kWeP8Z87Hssf3d7W

{
  "service": "http://apitest.onegovgever.ch/"
}

Ticket-Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "ticket": "ST-12345",
  "service": "http://apitest.onegovgever.ch/"
}

Der Server antwortet mit einen CAS ticket im JSON-Body, welches im nächsten Schritt vom Client bei dem Service gegen ein JWT Token eingelöst wird.

Einlösen des CAS-Tickets gegen ein JWT Token

Der Client kann nun das erhaltene CAS-Ticket beim Service (einem GEVER-Mandanten) über den @caslogin Endpoint gegen ein kurzlebiges JWT Token eintauschen:

Token-Request:

POST /@caslogin HTTP/1.1
Accept: application/json

{
  "ticket": "ST-12345",
  "service": "http://apitest.onegovgever.ch/"
}

Token-Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "token": "eyJhbGciOiJI..."
}

Dieses JWT Token kann vom Client nun für folgende Requests verwendet werden, um die Requests direkt am Service zu authentisieren.

API-Requests an den Service mit Token authentisieren

Für alle folgenden API-Requests an den Service authentisiert der Client diese nun, indem er das erhaltene JWT Token als Bearer Token im Authorization HTTP Header setzt:

API-Request:

GET / HTTP/1.1
Accept: application/json
Authorization: Bearer eyJhbGciOiJI...

API-Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "@id": "https://apitest.onegovgever.ch/",
  "...": "..."
}

Empfohlene Client-Implementierung

Die oben beschriebenen Schritte stellen den einfachen Fall dar, dass sich ein Client genau einmal authentisieren soll.

Für einen Client, der kontinuierlich authentisierte Requests durchführen soll, muss eine gewisse Logik implementiert werden um das Token regelmässig zu erneuern.

Der Client sollte, statt versuchen die Ablaufzeit des Tokens vorherzusagen, damit rechnen dass jeder Request aufgrund eines abgelaufenen Tokens scheitern kann. In diesem Fall soll er ein neues Token beziehen, und den Request mit dem neuen Token wiederholen.

Eine Beispiel-Implementation in Python für einen kontinuierlich authentisierenden Client:

Beispiel-Client (Python)
import requests
import time

SERVICE_URL = 'https://apitest.onegovgever.ch/'
PORTAL_URL = 'https://apitest.onegovgever.ch/portal'
LOGIN_URL = PORTAL_URL + '/api/login'
TICKET_URL = PORTAL_URL + '/api/cas/tickets'

USERNAME = "john.doe"
PASSWORD = "secret"


class Client(object):

    def __init__(self):
        self.portal_session = requests.Session()
        self.service_session = requests.Session()
        self.portal_session.headers.update({'Accept': 'application/json'})
        self.service_session.headers.update({'Accept': 'application/json'})

    def request(self, method, url, **kwargs):
        # First request will always need to obtain a token first
        if 'Authorization' not in self.service_session.headers:
            self.obtain_token()

        # Optimistically attempt to dispatch reqest
        response = self.service_session.request(method, url, **kwargs)
        if self.token_has_expired(response):
            # We got an 'Access token expired' response => refresh token
            self.obtain_token()
            # Re-dispatch the request that previously failed
            response = self.service_session.request(method, url, **kwargs)

        return response

    def token_has_expired(self, response):
        status = response.status_code
        content_type = response.headers['Content-Type']

        if status == 401 and content_type == 'application/json':
            return True

        return False

    def obtain_token(self):
        print("Obtaining token...")

        # Login to portal using /api/login endpoint
        self.portal_session.post(
            LOGIN_URL,
            json={"username": USERNAME, "password": PASSWORD}
        )

        # Get CSRF token that was returned by server in a cookie
        csrf_token = self.portal_session.cookies['csrftoken']

        # Send the CSRF token as a request header in subsequent requests
        self.portal_session.headers.update({'X-CSRFToken': csrf_token})
        self.portal_session.headers.update({'Referer': PORTAL_URL})

        # Once logged in to the portal, get a CAS ticket
        ticket_response = self.portal_session.post(
            TICKET_URL,
            json={"service": SERVICE_URL}
        )
        ticket = ticket_response.json()['ticket']

        # Use ticket to authenticate to the @caslogin endpoint on the service
        login_response = self.portal_session.post(
            SERVICE_URL + "/@caslogin",
            json={'ticket': ticket, 'service': SERVICE_URL}
        )

        # Get the short lived JWT token from the @caslogin response, and send
        # it as a Bearer token in subsequent requests to the service
        token = login_response.json()['token']
        self.service_session.headers['Authorization'] = 'Bearer %s' % token


def main():
    client = Client()

    # Issue a series of API requests an an example
    for i in range(10):
        response = client.request('GET', SERVICE_URL)
        print(response.status_code)
        time.sleep(1)


if __name__ == '__main__':
    main()