Authentication is carried out in one of two ways:
https://auth.maxiv.lu.se/login/
, which calls the endpoint /v1/login
when the user clicks Sign inPOST
containing username and password to the authentication endpoint (/v1/login
) directly
Upon receiving the login request through either method, the auth server uses the provided username and password to authentication the user against our AD. On a successful authentication, AD will respond with various user information such as username, name, email, group membership etc. The auth server includes this information as the payload of the JWT it creates, as well as some other information such as when it was created and when it expires. This information is then cryptographically signed and base-64 encoded. Finally, it is returned to the requestor as a cookie called maxiv-jwt-auth
, using the set-cookie
http header. This cookie is set to apply for all subdomains of maxiv, effectively making it a sso solution. By default, it will expired in 24 hours.
In the application requiring authentication, the app server needs to make a request to the /v1/decode
endpoint of the auth server to verify that the content of the JWT has not been tampered with. This needs to be done on each request that requires authorization. The decode call also returns the decoded payload, which can be used to determine user authorization (e.g. by group membership).
The jwt secret is stored in ansible vault, and is decrypted and copied from here onto the auth server (w-v-imsdocker-3) as part of the deployment.
For most web application integrations, method 1 would be used. Below is a more detailed example of how this is typically implemented.
https://auth.maxiv.lu.se/login/?redirect=my-app.maxiv.lu.seIf the user follows this link and successfully authenticates, a cookie named
maxiv-jwt-auth
will be accessible in your app. Because the user is returned with a redirect, your page will be reloaded right after the cookie is set. This makes a startup script a suitable location to check for the presence of this cookie.decode
request. If not found, inform the user that they are not logged in and render the login link.Login can be performed by MAX IV staff and active DUO users. These two user groups can be distinguished by the isStaff
flag in the decoded token. Applications integrating the auth SSO should consider which parts of their web applications, if any, should be accessible by non MAX IV staff.
$( document ).ready()
, and document.onload()
in vanilla js. Note that on line 1, the cookie is read using a library called universal-cookie. Parsing cookie values in vanilla js is a bit more complicated, see for example https://stackoverflow.com/questions/10730362/get-cookie-by-name
const jwt = cookies.get("maxiv-jwt-auth"); //Check for an existing jwt cookie if (jwt){//If found, verify the content against the decode endpoint. This also includes checking if the token has expired const resp = await fetch("https://auth.maxiv.lu.se/v1/decode", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ jwt }), }); if (!resp.ok) { throw new Error("Invalid or expired jwt token"); //Alternatively, redirect them directly to the login page window.location.replace(`https://auth.maxiv.lu.se/login?redirect=${window.location.href}`); } return await resp.json(); //object containing name, username, email, phone, isStaff, groups }else{//if not found, redirect them to the auth login page window.location.replace(`https://auth.maxiv.lu.se/login?redirect=${window.location.href}`); }
A backend implementation would look very similar, but returning a http 401 if the token is missing or invalid. If you're using a framework that supports middleware, it might be a good idea to add authorization here. In nodejs it might look like this:
app.use("/", async function (req, res, next) { const resp = await fetch("https://auth.maxiv.lu.se/v1/decode", { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ "maxiv-jwt-auth": req.cookies["maxiv-jwt-auth"] }) }); if (!resp.ok) { const err = new Error("Authentication required"); res.statusCode = 401; res.statusMessage = "Authentication required"; return next(err); } else { req.jwt = await resp.json(); next(); } } );Note that the jwt payload is also made available in all endpoints by attaching it to the http request object in a property called jwt.
MAX IV LDAP authentication API
This is how you request a new JWT for a user.
Current lifetime is 24 hours.
If the token is missing, invalid or has expired, returns status code 401 (Unauthorized)
POST /v1/login
{
"username": "<username>",
"password": "<password>",
"maxAge": "<maxAgeInSeconds>" //defaults to 60 * 60 * 24 if omitted
}
{
"jwt": "eeeeee.ffffff.cccccc"
}
The response also includes the set-cookie
header, with the
following values:
Set-cookie: maxiv-jwt-auth=<jwt>; Domain=maxiv.lu.se; Max-Age=86400000; Path=/
This means the cookie is valid for 24 hours on maxiv.lu.se and all of its
subdomains.
By extending the request with includeDecode
set to
true
, the JSON response will include the decoded jwt token.
POST /v1/login
{
"username": "<username>",
"password": "<password>",
"includeDecode": true
}
{
"jwt": "eeeeee.ffffff.cccccc",
"exp": 1557474243,
"iat": 1557470643,
"email": "<email>",
"name": "<full name>",
"phone": "<phone>",
"username": "<username>",
"isStaff": true/false,
"groups": [
"KITS",
"Users",
"Beamlines",
...
],
}
Deletes the jwt cookie
POST /v1/logout
Set-Cookie: maxiv-jwt-auth=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT
This is how you extend an already existing JWT for a user.
This works for as long as the token is valid.
If the token is missing, invalid or has expired, returns status code 401 (Unauthorized)
POST /v1/extend
{
"jwt": "aaaaa.bbbbbb.cccccc"
}
{
"jwt": "eeeeee.ffffff.cccccc"
}
The response also includes the
set-cookie header, with the
following values:
Set-cookie: jwt=<jwt>; Domain=maxiv.lu.se; Max-Age=3600; Path=/
This means the cookie is valid for 1 hour on maxiv.lu.se and all of its
subdomains.
Validates and decodes an existing jwt token. If the token is missing, invalid or has expired, returns status code 401 (Unauthorized)
POST /v1/decode
{
"jwt": "aaaaa.bbbbbb.cccccc"
}
{
"jwt": "eeeeee.ffffff.cccccc",
"exp": 1557474243,
"iat": 1557470643,
"email": "<email>",
"name": "<full name>",
"phone": "<phone>",
"username": "<username>",
"isStaff": true/false,
"groups": [
"KITS",
"Users",
"Beamlines",
...
],
}
The JWT got a part that is the payload. The structure of the payload is a JSON object with the following format.
{
"jwt": "eeeeee.ffffff.cccccc",
"exp": 1557474243,
"iat": 1557470643,
"email": "<email>",
"name": "<full name>",
"phone": "<phone>",
"username": "<username>",
"isStaff": true/false,
"groups": [
"KITS",
"Users",
"Beamlines",
...
],
}