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:
- Authentisieren am Portal mit Benutzername und Passwort
- Beziehen eines CAS-Tickets vom Portal
- Einlösen des CAS-Tickets gegen ein kurzlebiges Token (JWT) beim Service (GEVER-Mandant)
- 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:
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()