An easy-to-use oauth2 client, written in pure nim
  • Nim 99.8%
  • Shell 0.2%
Find a file
2026-08-12 06:03:07 +02:00
example Initial commit; release 0.1.0 2026-08-11 18:42:56 +02:00
src/ninox Initial commit; release 0.1.0 2026-08-11 18:42:56 +02:00
tests Initial commit; release 0.1.0 2026-08-11 18:42:56 +02:00
.gitignore Initial commit; release 0.1.0 2026-08-11 18:42:56 +02:00
config.nims Initial commit; release 0.1.0 2026-08-11 18:42:56 +02:00
docs.sh Initial commit; release 0.1.0 2026-08-11 18:42:56 +02:00
LICENSE Initial commit; release 0.1.0 2026-08-11 18:42:56 +02:00
ninox_oauth.nimble Update ninox_oauth.nimble 2026-08-12 06:03:07 +02:00
Nittofile Initial commit; release 0.1.0 2026-08-11 18:42:56 +02:00
readme.md Initial commit; release 0.1.0 2026-08-11 18:42:56 +02:00

ninox.nim-oauth

An oauth client for nim.

License

This project is licensed under AGPL-3.0-or-later. You should have recieved a copy of the License together with this file, named LICENSE. If not, please see the terms of the license here: https://www.gnu.org/licenses/agpl-3.0.en.html

Usage

Creating the client

A client can be created via a builder-pattern with newOAuthClientBuilder:

let maybe_client = newOAuthClientBuilder
    .withClientId("...")
    .withClientSecret("...".ClientSecret)
    .withAuthUrl("http://example.com/auth".parseUri)
    .withTokenUrl("http://example.com/token")
    .build() # Build returns a `Result[OAuthClient, string]`.

assert maybe_client.isOk

let client = maybe_client.get

Additionally, you can use .withRedirectUrl("...") to set an default redirection url.

After configuring your client (as seen above), a authorization url needs to be build, which is then presented to the user; often this "presentation" is an redirect to the url.

# Creates an pkce challenge & verifier using sha256 and a 32 byte random secret.
# The length can be any value between 32 and 96 (inclusive).
let (challenge, verifier) = newPkceCodeChallenge(32)

# Create an random "state" token which is used to prevent against csrf attacks.
let state = randomCsrfToken()

# Build the final url
let url = client.authorizeUrl(state)
    .withRedirectUrl("...") # Optional if set in the client
    .withPkceChallenge(challenge)
    .build()

Store both the state and verifier variable in an session, as they're needed in the callback; once the user has completed authentication, the authentication service redirects to the configured redirect url:

# Saved from the previous steps; retrieved from a session or similar.
let saved_state, saved_verifier = ...

# The callback url MUST contain atleast two query params:
# `state` and `code`; these variables represent those:
let state, code = ...

# The state MUST be checked to prevent csrf attacks:
if state != saved_state:
    raise newException(CatchableError, "CSRF did not match")

# Now we can exchange the code for an token.
#
# Syncronous request using HttpClient;
# async variant available using .request_async()
# aswell as an an multisync variant with .request(http_client)
let maybe_token = client.exchange_code(code, saved_verifier)
    .request_sync[:TokenResponse]() # Must specify the response type;
                                    # can be a custom one, aslong as it inherits from TokenResponse.

# The request methods return Result[TokenResponse, EndpointError]
assert maybe_token.isOk
let token = maybe_token.get

Example application

This repository ships with an example application that employs the prologue webframework to implement an basic service that utilizes this package for authentication.

All it needs is an config.json file in it's working directory:

{
    "id": "...", // client id
    "secret": "...", // client secret
    "auth-url": "https://example.com/oauth/authorize", // The authorization url
    "token-url": "https://example.com/oauth/token", // The token url
    "port": 9000 // Listenting port of the service
}

Features:

  • Code Grant via PKCE is handled via login_pkce and callback_pkce. Callback url is http://localhost:<port>/callback-pkce; make sure to whitelist this in your authentication server.

Advanced

These are advanced topics / documentation. You generally dont need this if you just want to use the library; however, you might want or need to customize certain aspects. In that case, you're in the right place:

Custom http client

The library is written with extensibility in mind; as such, the http interface is plugable (to a degree).

Each component that does an http request supports a .build_request() besides it's .request methods; and is defined as follows (example for CodeTokenRequest / client.exchange_code()):

func build_request*(req: var CodeTokenRequest): (HttpMethod, string, HttpHeaders, string) =
    ...

It returns a tuple with:

  • the http method
  • the request uri (as string)
  • the http headers
  • the (encoded) body as string

Likewise, response handeling is done via the process_endpoint_response function:

proc process_endpoint_response*[T](status: string, headers: HttpHeaders, body: string): EndpointResult[T] =
    ...

Note: status might be replaced with the raw HttpCode in the future.

For example, the .request for CodeTokenRequest / client.exchange_token() is as follows:

proc request*[T: TokenResponse](req: sink CodeTokenRequest, client: HttpClient | AsyncHttpClient): Future[EndpointResult[T]] {.multisync.} =
    let (httpMethod, uri, headers, body) = req.build_request()
    let resp = await client.request(uri, httpMethod = HttpPost, headers = headers, body = body)
    return await process_endpoint_response[T](resp)

Using this informations, any 3rd party http client can be utilized using this library.

Custom TokenResponse

The default TokenResponse type just represents the token endpoint response as defined in RFC 6749, Section 5.1. If you have an authentication server that adds additional fields you need to extract, you have two options:

  • Use ExtraTokenResponse; it's designed to collect all unknown fields into it's field extra_fields. Note however that it captures ALL unknown fields and generally has no validation for those fields. If your needs are outside of that, you can use the second method:

  • Extend TokenResponse; since it inherits from RootObj, inheritence and OOP are allowed. The library utilizes https://github.com/guzba/sunny for json serialization, so you can do quite a lot.

    For example:

    type
      MyTokenResponse = ref object of TokenResponse
          id_token* {.json: ",required".}: string
    

    Using MyTokenResponse would allow you to use .request / .process_endpoint_response and get deserialization & some validation for free.