Authentication and Authorization Flow

Authentication is carried out in one of two ways:

  1. By redirecting the user to the login UI located at https://auth.maxiv.lu.se/login/, which calls the endpoint /v1/login when the user clicks Sign in
  2. Making a http POST 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.

  1. In you app, add a login link directing the user to
    https://auth.maxiv.lu.se/login/?redirect=my-app.maxiv.lu.se
    If 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.
  2. In your app, check for the presence of this cookie, and if found, perform a decode request. If not found, inform the user that they are not logged in and render the login link.
  3. If the decode request is successful (status code 200) the user is authenticated. Additional information such as name, email and AD group membership can also be read from the response.
  4. On the server side, any request that requires that the user is authenticated also needs to perform a decode to verify that the jwt has not been tampered with on the client side. Note that cookies are automatically attached as a header to server requests.

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.

Keep in mind that jwt should not be used to store any sensitive information. The token is base64 encoded and cryptographically signed, but not encrypted! For more information about jwt in general, visit the official site. For more information about how it is being used in the MAX IV auth service, continue reading the API documentation below.

Reference front-end implementation

Below is an auth reference implementation in JavaScript. This code is intended to run as the app loads. In a react app this would be on the mounting of the main component. The equivalent in jquery would be $( 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.

JWT Auth - API v1

MAX IV LDAP authentication API

Login

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)

Request

POST /v1/login
{
    "username": "<username>",
    "password": "<password>",
    "maxAge": "<maxAgeInSeconds>" //defaults to 60 * 60 * 24 if omitted
}
    

Response

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

Include the decoded jwt

By extending the request with includeDecode set to true, the JSON response will include the decoded jwt token.

Request

POST /v1/login
{
    "username": "<username>",
    "password": "<password>",
    "includeDecode": true
}

Response

{
    "jwt": "eeeeee.ffffff.cccccc",
    "exp": 1557474243, 
    "iat": 1557470643, 
    "email": "<email>",
    "name": "<full name>",
    "phone": "<phone>",
    "username": "<username>",
    "isStaff": true/false,
    "groups": [
        "KITS",
        "Users",
        "Beamlines",
        ... 
    ],  
}

Logout

Deletes the jwt cookie

POST /v1/logout

Response

The response contains the following response header, clearing and invalidating the cookie:
Set-Cookie: maxiv-jwt-auth=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT

Extending

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)

Request

POST /v1/extend
{
    "jwt": "aaaaa.bbbbbb.cccccc"
}

Response

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

Decode

Validates and decodes an existing jwt token. If the token is missing, invalid or has expired, returns status code 401 (Unauthorized)

Request

POST /v1/decode
{
    "jwt": "aaaaa.bbbbbb.cccccc"
}

Response

{
    "jwt": "eeeeee.ffffff.cccccc",
    "exp": 1557474243, 
    "iat": 1557470643, 
    "email": "<email>",
    "name": "<full name>",
    "phone": "<phone>",
    "username": "<username>",
    "isStaff": true/false,
    "groups": [
        "KITS",
        "Users",
        "Beamlines",
        ... 
    ],  
}

Payload format

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