- Nim 99.8%
- Shell 0.2%
| example | ||
| src/ninox | ||
| tests | ||
| .gitignore | ||
| config.nims | ||
| docs.sh | ||
| LICENSE | ||
| ninox_oauth.nimble | ||
| Nittofile | ||
| readme.md | ||
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.
Authorization Code Grant with PKCE (Recommended)
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_pkceandcallback_pkce. Callback url ishttp://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
HttpCodein 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 fieldextra_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 fromRootObj, 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".}: stringUsing
MyTokenResponsewould allow you to use.request/.process_endpoint_responseand get deserialization & some validation for free.