Authentification CAS via le portail

Le protocol CAS et un protocol de Single-Sign-On qui permet de s’authentifier à un client GEVER via le portail.

Processus d’authentification

L’authentification se déroule en 4 étapes:

  1. Authentification au portail avec nom d’utilisateur et mot de passe.
  2. Obtention d’un ticket CAS du portail.
  3. Echange du ticket CAS contre un JWT (JSON Web Token) éphémère auprès du service en question (client GEVER).
  4. Utilisation du JWT pour l’authentification des requêtes suivantes au service.

Authentification au portail

Pour obtenir un ticket CAS, un client doit commencer par s’identifier au portail. Ceci se fait avec l’endpoint /api/login du portail:

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":""}

Le client est ensuite authentifié au portail par un Session-Cookie. Le client HTTP doit donc renvoyer avec les requêtes suivantes les cookies obtenus du portail.

Le portail livre également, en plus du Session-Cookie, un CSRF-Token sous forme de cookie. Celui-ci doit être extrait par le client et inclus dans les requêtes suivantes au portail dans le HTTP Header X-CSRFToken.

Le client doit également indiquer l’URL du portail dans le Referer HTTP Header, sinon les requêtes seront déclinées par le méchanisme de protection CSRF.

Obtention d’un ticket CAS du portail

Le client peut obtenir un ticket CAS du portail pour le service souhaité via l’endpoint /api/cas/tickets.

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/"
}

Le serveur répond avec un ticket CAS dans le JSON-Body, qui peut ensuite être échangé auprès du service contre un JWT (voir prochaine étape).

Echange du ticket CAS contre un JWT

Le client peut maintenant échanger le ticket CAS contre un JWT (JSON Web Token) éphémère auprès du service en question (client GEVER) via l’endpoint @caslogin.

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..."
}

Ce JWT peut ensuite être utilisé par le client pour authentifier les requêtes suivantes directement auprès du service.

Requêtes API au service authentifiées avec le JWT

Le client authentifie toutes les requêtes utlérieures à l’API du service en incluant le JWT obtenu comme Bearer Token dane le HTTP Header Authorization:

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/",
  "...": "..."
}

Implémentation conseillée pour un client

Les étapes décrites ci-dessus représentent le cas le plus simple, d’un client qui veut s’authentifier une seule fois.

Une certaine logique pour actualiser régulièrement le JWT doit être implémentée pour un client qui veut continuellement exectuer des requêtes authentifiées.

Au lieu d’essayer de prédire l’expiration du JWT, le client devrait s’attendre à ce que n’importe quelle requête puisse échouer à cause d’un JWT plus valable. Il devrait alors obtenir un nouveau Token avant de réessayer la requête en question.

Voici un exemple d’implémentation d’un tel client en Python:

Exemple de 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()