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:
- Authentification au portail avec nom d’utilisateur et mot de passe.
- Obtention d’un ticket CAS du portail.
- Echange du ticket CAS contre un JWT (JSON Web Token) éphémère auprès du service en question (client GEVER).
- 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:
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()