SOGo API Authentication

Authentication: Verifying the identity or validity of a client/user.
Authorization: Verifying that an authenticated client/user has the right to access.

Among all the API endpoints, only a few can be done without being authenticated. That’s why, contrary to the usual Flask auth extension, the authentication is not made inside a decorator for each endpoint but inside a @before_request at the API blueprint.

Authentication flow

In brief, a user will authenticate using the /api/user/v1/auth APIs.

If successful, SOGo API will generate a JWT token. The client will have to send the JWT token for each request inside the Authorization header:

Authorization: Bearer <JWT Token>

As SOGo API must be modular and agnostic, some parts of the code are generic and only later the JWT Token is implemented. Even if there is no other way to handle authentication for now.

Also keep in mind that, maybe later, the authentication will not be in the HTTP layer but below like on the TLS layer.

Authentication mechanism

SOGo 6 can have different authentication mechanisms for different domains. Each domain only has one authentication mechanism, though.

The first request is to get the mechanism:

GET /api/user/v1/auth/mode?username=myuser@test.com&redirect=/home
  • username is the UID of the user. Can be the mail or anything if SOGo is configured for domainless login. If the username is not found, no error will be returned, to avoid attackers checking real usernames. In that case, the endpoint always returns code 200 with the plain mode.

  • redirect is the client/UI URL to redirect to after the whole SSO flow, that is handled by the API. Not needed if SOGo only does plain authentication.

If SOGO_S_DOMAINLESS_LOGIN is True, the username in query can be without domain. SOGo will have to find the user first among the user source to get the domain.

Plain or user/domain not found

The return is simple and contains only the auth_mode:

{
    "auth_mode": "plain"
}

OpenID:

The return, besides the auth_mode, also has the authorization_url. The UI/Client must make a GET request to this URL. The rest of the JSON has the different parameters of the query.

{
    "auth_mode": "openid",
    "authorization_url": "https://myopenid.com/oauth2/authorize?...",
    "scope": "openid profile email",
    "client_id": "myClient",
    "state": "eaothjshqoj.dsqkj",
    "redirect": "https://my.api/api/user/v1/auth/callback/domain"
}

Redirection after an AJAX request triggers CORS Policy, hence the lack of 3XX code status. 200 is used.

Authentication flow

Plain method

The endpoint is:

POST /api/user/v1/auth/login

This endpoint expects a JSON:

{
    "username": "username",
    "password": "password"
}

If successful, it will return the SOGo JWT token in the body.

OpenID method

  1. UI makes the GET request on auth/mode with a UI redirect URL to itself in the query.

  2. API creates a state value that would be checked later. It returns 200 with the authorize_url. This URL contains the state in the query and the user’s domain id.

  3. UI makes a GET request on the authorize_url.

  4. User sees the OpenID login page. User logs in.

  5. OpenID server returns a 302 on the API auth/callback/domain. domain is the id of the user domain.

  6. API checks the domain, checks the state value.

  7. API fetches the access_token. Creates a User Session and a JWT token.

  8. API returns a 302 with the JWT token in a cookie and Location to UI redirect.

  9. UI gets the JWT token from the cookie and now uses it in the Authorization header.

SOGo Authenticated request

Even if SOGo currently uses JWT token, it’s important to keep the parent class abstract in case of a future feature without a JWT token

The terms used:

  • User: Class of a user, authenticated or not. Instantiated in SOGo backend for each request.

  • Voucher: Class of what is given to the API client to authenticate a User (JWT token for now)

  • UserSession: Class of what is stored in the API. Data of UserSession allows instantiating a User (UserSession is stored in Redis, the cache)

  • VoucherUserService: Class that generates a Voucher and UserSession from a User. Or generates a User from a Voucher: Voucher gets a UserSession that gets a User.

The UserSession cannot be directly in the Voucher because it has sensitive data like the passwords. The UserSession must be encrypted to not store the sensitive data in plain text. The UserSession is encrypted by a symmetric-key algorithm. The session key, is not stored but given to the Voucher.

The session key in a Voucher is also encrypted with a secret of the SOGo API environment.

That way, each time a client makes a request with a Voucher, the Voucher is used to decrypt the UserSession.

  • UserSession alone is secured.

  • Voucher alone is secured as it doesn’t have any sensitive info of the User.

  • UserSession + Voucher are secured as you can’t decrypt the session key of the Voucher

  • UserSession + Voucher + Secret: not secured but you have a serious problem.

What if a Voucher is stolen? It could be used by another client to impersonate the original user?

Yes, other protections can be added:

  • Associate a Voucher to an IP; if the IP changes, make the user log in again.

  • Reduce the expiration time of the Voucher, so even if stolen, it will be quickly expired.