How to authenticate?¶
Before you can use any of the Obelisk APIs, you will need to authenticate the user or your client itself. After this authentication step, you will be able to acquire an Access Token that can be used to access the Obelisk APIs.
Creating a Client¶
The first thing you will need is a Client. You can create a client yourself, using the Obelisk Catalog UI. You can create a personal client or - if you are part of a Team - a team client. Both have the same functionality.
All your personal clients however will share a common personal usage limit1 pool, whereas your team clients will each get an individual instance of the "team usage plan"-granted usage limits pool.
Tip
Team clients are not automatically better! A Team usage plan might be very restrictive compared to your personal usage limit shared by all your personal clients.
Client options: confidential, public, authenticate-as-user, authenticate-as-client
More important are the options you assign to your client. They determine the type of client you have, and the required authentication steps. Follow the table below to understand what the options mean.
option | value | description |
---|---|---|
confidential | yes | Confidential clients don't expose their code publicly, they are running in safe environment. This means they can keep a client_secret hidden in the code. |
confidential | no | Public clients expose their code publicly, or can not be trusted to not be manipulated, hence they cannot keep a client_secret hidden. They must use the PKCE extension to login with a code_challenge . |
authenticate users | yes | When your client facilitates users to access their content from Obelisk, then your app is said to authenticate users . This means a user will be redirected to Obelisk Login, and you client get authorization on behalf of the users to fetch their content. |
authenticate users | no | When your client only needs to access content tied to the client itself, then it will not authenticate users . |
Warning
Native Android and iOS Apps are not running in a safe environment, so they are not confidential. They are distributed and run on phones that you don't have any control over. They can easily be manipulated with man-in-the-middle style attacks. They are public clients and require the use of the PKCE Extension protocol.
The following table lists some use cases and what options the clients need.
confidential | authenticate users | client type | examples |
---|---|---|---|
yes | yes | Backend-hosted client2 using Oblx User's content | eg. Java Application server hosting a user-based web-application, Python Flask user-based server-side web-application |
yes(*) | no(*) | Backend-hosted client using it's own data | eg. Backend Java application sending data, Python backend application sending data, Backend Cron-timed bash script analysing data batches |
no | yes | Client-side web application using Oblx User's content | eg. User-based dashboard Single Page Application, Obelisk Catalog UI, Native Android or iOS application that uses User data. |
no | no | Client-side web application using it's own data | eg. Client-side Javascript web-application consuming it's own data, Client built in to Native Android or iOS applications that sends data. |
Info
(*) A confidential client using its own data can directly skip ahead to requesting an Access Token.
In all but one of the cases above, you will have to follow a 2-step procedure to get an Access Token. (The exception is the confidential+own-data use case marked with (*))
The first step will see you getting a proof of authentication in the form of an authorization code
.
The second step will allow you to exchange that proof of authentication for a Access Token.
Step 1: Authentication¶
To initiate a login, you need to point the User Agent (eg. browser) to the Authentication URL. This will open the Obelisk Authentication page for users to log in.
In case you have a public client that needs to authenticate by itself, you just GET the URL in you code, it will immediately respond with an authorization code
on your registered redirect_uri
.
After user login, the redirect_uri
will also be called with a proper authorization code
added to that redirect_uri
as a query parameter along with the state that you passed in the initial request.
https://redirect_uri
?code=
afjAae4Qsde3qWS2i&state=
L2hvbWU
Tip
This state is useful if you want to navigate back to a certain location of your application once authentication is over.
If you are using a non-confidential client, you are required to use the PKCE Extension during the authentication procedure. This is a secure alternative to using a client_secret. Please read up on how to do this in the PKCE Extension section below.
Important
Technical information on how to construct this Authentication URL can be found in the API Documentation.
Step 2: Token¶
You now have a proof of authentication, either as an authorization code
retrieved in Step 1 or in the form of a client credential pair (clientId
and clientSecret
) for confidential clients using their own data.
You can now send this proof of authentication to the Token endpoint to retrieve an Access Token as described below.
- In all but one case, this will require you to set the
grant_type
toauthorization_code
to send youcode
in the request. - Only confidential clients acting on their own behalf will have skipped the authentication step and will have to use their
client_id
andclient_password
as proof of authentication. They will have to set thegrant_type
toclient_credentials
and add the Base64 encoded string of theclient_id:client_secret
pair in the Authorization header like this:Authorization: Basic
base64EncodedString
If successful, a JSON object is returned. It contains a few keys, one of them (access_token
) is your Access Token for the Obelisk APIs.
To use the APIs add the Authorization Bearer header with the token to each request:
Authorization: Bearer
yourAccessToken
You will also get an IDToken, this is a JWT. It is digitally signed so the Obelisk backend can check if it has been tampered with. It is primarily used to customize your client to some of the user's profile details.
Response:
{
"token_type": "bearer",
"access_token" : "XNQNtuEZDfarzeDN",
"id_token" : "eyJraWQiOiI4MWQ5MjhjYi1hNjQ4LTQyMDktYjk4OC00NjE0NDY2YTYyZTUiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJjNjA1YjQ2OGYzZGQ1MTY2NmJjOTM4MGU5IiwibmFtZSI6IlB1YmxpY0FzSXRzZWxmIiwiZW1haWwiOiJ0ZXN0ZXJAb2JseC5pbyIsImlkcCI6ImxvY2FsIiwidHlwZSI6ImNsaWVudCIsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hdXRoIiwiYXVkIjoiUHVibGljQXNJdHNlbGYiLCJleHAiOjE2MTkxODY1NzYsImp0aSI6Ik13YTJ0Tl9oZDNUaEhqZUdaOU5vTEEiLCJpYXQiOjE2MTY1OTQ1NzYsIm5iZiI6MTYxNjU5NDQ1Niwic2lkIjoiSVVUM0xITzhYRFl4MGk2ZyJ9.fH-ZBytOI6rBrH-CCBixpIvmD-MGflELiz2Kn1qiGfRREOT6wpGrEmxueyYLmhJDJkLswrcCdhKgM747MvPB4rkoRVM4DlYpDKzl4mIsb0bua1hC-TkxG5aW8W9ZUNj1vMXotDDnLPQhWtOPFDxyum2qdu8OHzQmTENuo72ujuegF4irv4BahW33fVUk7YznSHmoRwjFGqtzcvPZCfpD30fe-Q42aTQlGKig7cavVx5wFs2FNljdv9ft41g5HoneRTdRIDtsjYtMbk3z4OSE9uFIlGFjReAsIOn0ZvRKlrbCBb027kDHjQn15ZJvaBNlpuJorINalsKgrFs6fCCYPw",
"max_idle_time" : 3600,
"expires_in" : 3600,
"max_valid_time": 86400,
"remember_me" : false
}
token_type
: The type of access_token. Helps to use it correctly.access_token
: Your access_token to use as Bearer Token for Obelisk API callsid_token
: Your id_token, used for customizing your application to some user valuesmax_idle_time
: Maximum time (in seconds) the access_token stays valid, if not being used on API calls.expires_in
: Maximum time (in seconds) the access_token stays valid, if not being used on API calls. (alias formax_idle_time
).max_valid_time
: Maximum time (in seconds) the access_token stays valid, even if it is being used withinmax_idle_time
windows.remember_me
: Tells your application if the users requested to remember his/her session or not. It is up to your own application to support this or not.
Important
Technical information on the Token Endpoint can be found in the API Documentation.
PKCE Extension¶
Since it is not safe to store a password in the code of a non-confidential client, this method makes use of the fact that we are executing a 2-step protocol.
Step by step:
- Your client will construct a random string called the
code_verifier
- Your client will hash this string using SHA-256 and call the result the
code_challenge
- Your client will send the
code_challenge
and thecode_challenge_method
(SHA-256
) as extra parameters with the authentication URL request. - The backend will store these and keep them for later.
- When your client receives the authentication
code
and sends it back to the Token endpoint in step 2, it will send along the originalcode_verifier
. - The backend will hash the received
code_verifier
with the method stored and compare it with your originally sentcode_challenge
. - If they match, the backend is sure that it was talking to the same client during this protocol and a man-in-the-middle-attack is prevented.
More on this extension can be read in the official RFC 7636.
Creating a code_verifier¶
The recommendation in the RFC goes as follows:
Appendix A. Notes on Implementing Base64url Encoding without Padding
This appendix describes how to implement a base64url-encoding function without padding, based upon the standard base64-encoding function that uses padding.
To be concrete, example C# code implementing these functions is shown below. Similar code could be used in other languages.
static string base64urlencode(byte [] arg) { string s = Convert.ToBase64String(arg); // Regular base64 encoder s = s.Split('=')[0]; // Remove any trailing '='s s = s.Replace('+', '-'); // 62nd char of encoding s = s.Replace('/', '_'); // 63rd char of encoding return s; }
Here are some code examples on how to create a proper code_verifier
.
-
Kotlin
private fun createCodeVerifier(): String { val oct32 = Random.nextBytes(32) val code_verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(oct32) return code_verifier }
-
Java
private String createCodeVerifier() { Random rg = new Random(); byte[] oct32 = new byte[32]; rg.nextBytes(oct32); String code_verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(oct32); return code_verifier; }
-
TypeScript
const cr = window.crypto; const randomOctets = new Uint8Array(32); cr.getRandomValues(randomOctets); const codeVerifier = b64urlFromBuffer(randomOctets);
Creating a code_challenge¶
The recommendation in the RFC goes as follows:
4.2. Client Creates the Code Challenge The client then creates a code challenge derived from the code verifier...
S256 code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
If the client is capable of using "S256", it MUST use "S256", as "S256" is Mandatory To Implement (MTI) on the server.
Here are some code examples on how to properly calculate the code_challenge
hash.
-
Kotlin
/** SHA-256 encoding. Using DigestEngine class from https://util.jodd.org/, but can be any SHA256 MessageDigest Engine */ fun S256(codeVerifier: String): ByteArray { return DigestEngine.sha256().digest(codeVerifier) } /** Base64url encoder without padding */ fun base64urlEncode(arg: ByteArray): String { return Base64.getUrlEncoder().withoutPadding().encodeToString(arg); } val code_challenge = base64urlEncode(S256(codeVerifier))
-
Java
private byte[] S256(String codeVerifier) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] enc = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8)); return enc; } private String base64urlEncode(byte[] arg) { return Base64.getUrlEncoder().withoutPadding().encodeToString(arg); } private String createCodeChallenge() { String code_challenge = null; try { code_challenge = base64urlEncode(S256("test")); } catch (NoSuchAlgorithmException ex) { // Wrong algorithm } return code_challenge; }
-
TypeScript
private async getCodeChallenge(codeVerifider: string): Promise<string> { const codeChallenge = b64urlFromBuffer(await this.digestMessage(codeVerifier)); return Promise.resolve(codeChallenge); } private async digestMessage(message: string) { const msgUint8 = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); return hashBuffer; }
Login to existing session¶
A user can login to his/her existing session by sending the id_token
. If you want to support this in your client, you need to store the id_token
of the user in browser storage.
You can then send it along with the authentication url as a query parameter id_token
.
The handling for your client is identical, the only difference is that the user will not be prompted with a login if there is a session still active on the backend.
-
Personal usage limits are calculated based on the personal usage limits that apply to your user and any user usage limits granted by usage plans of Teams that you are a member of. ↩
-
These clients are running in a safe environment within you organization. This means a typical backend server running on your own premise, or on a cloud environment only your organization has access to. A mobile phone is not a safe environment. ↩