Skip to content

elmish/oidc

Repository files navigation

Elmish-OIDC

Build

Authorization Code Flow with PKCE component for Elmish applications — browser, .NET, and React Native.

Packages

Package Platform NuGet
Fable.Elmish.OIDC Browser (Fable) NuGet
Elmish.OIDC .NET (WPF/MAUI) NuGet
Fable.Elmish.OIDC.ReactNative React Native (Expo/Fable) NuGet

Features

  • Authorization Code + PKCE (OAuth 2.1) — Implicit Flow is not supported
  • OIDC Discovery — endpoints resolved automatically from .well-known/openid-configuration
  • Client-side JWT validation — RS256 signature verification, issuer/audience/expiry/nonce claim checks
  • Silent token renewal — iframe (browser), refresh token (native)
  • Session resume — stored tokens revalidated on app start
  • Configurable — clock skew tolerance, algorithm allowlist, custom storage, renewal timing

Installation

# Browser (Fable)
dotnet add package Fable.Elmish.OIDC

# .NET (WPF/MAUI)
dotnet add package Elmish.OIDC

# React Native (Expo)
dotnet add package Fable.Elmish.OIDC.ReactNative

Usage

Configuration

open Elmish.OIDC.Types

let oidcOptions : Options =
    { clientId = "my-app"
      authority = "https://idp.example.com"
      scopes = [ "openid"; "profile"; "email" ]
      redirectUri = "https://myapp.com/callback"
      postLogoutRedirectUri = Some "https://myapp.com"
      silentRedirectUri = Some "https://myapp.com/silent-renew.html"
      renewBeforeExpirySeconds = 300
      clockSkewSeconds = 300
      allowedAlgorithms = [ "RS256" ] }

Browser (Fable)

open Elmish
open Elmish.OIDC
open Elmish.OIDC.Types

let oidc = Api.create oidcOptions

type Model = { oidc: Model<UserInfo> }
type Msg = OidcMsg of Msg<UserInfo>

let init () =
    let m, c = oidc.init
    { oidc = m }, Cmd.map OidcMsg c

let update msg model =
    match msg with
    | OidcMsg m ->
        let m', c = oidc.update getUserInfo m model.oidc
        { model with oidc = m' }, Cmd.map OidcMsg c

let subscribe model =
    oidc.subscribe model.oidc |> Sub.map "oidc" OidcMsg

Host a silent renewal page at silentRedirectUri:

<!DOCTYPE html>
<html><body>
<script>parent.postMessage(location.search, location.origin)</script>
</body></html>

.NET (WPF/MAUI)

Uses loopback redirect (RFC 8252) and refresh token renewal.

open Elmish
open Elmish.OIDC
open Elmish.OIDC.Types

let nav = DotNet.Navigation.loopback 8912
let storage = DotNet.memoryStorage ()
let oidc = Api.create nav storage oidcOptions

let init () =
    let m, c = oidc.init
    { oidc = m }, Cmd.map OidcMsg c

let update msg model =
    match msg with
    | OidcMsg m ->
        let m', c = oidc.update getUserInfo m model.oidc
        { model with oidc = m' }, Cmd.map OidcMsg c

let subscribe model =
    oidc.subscribe model.oidc |> Sub.map "oidc" OidcMsg

React Native (Expo)

Uses expo-web-browser for in-app authentication and refresh token renewal.

Runtime requirements. The default React Native runtime does not ship the Web Crypto API (globalThis.crypto.subtle). Install and register react-native-quick-crypto at your app entry point, before importing this library:

import { install } from 'react-native-quick-crypto'
install()

Then call ReactNative.ensureCrypto () once at startup for an early, actionable error if the polyfill is missing.

open Elmish
open Elmish.OIDC
open Elmish.OIDC.Types

let nav = ReactNative.Navigation.authSession oidcOptions.redirectUri
let storage = ReactNative.memoryStorage ()
let oidc = Api.create nav storage oidcOptions

let init () =
    let m, c = oidc.init
    { oidc = m }, Cmd.map OidcMsg c

let update msg model =
    match msg with
    | OidcMsg m ->
        let m', c = oidc.update getUserInfo m model.oidc
        { model with oidc = m' }, Cmd.map OidcMsg c

let subscribe model =
    oidc.subscribe model.oidc |> Sub.map "oidc" OidcMsg

API

Core (all platforms)

Function Description
Api.create ... Create API with platform-specific defaults
api.init Initialize OIDC model and commands
api.update getUserInfo msg model Handle OIDC messages
api.subscribe model Renewal timer subscription

Session queries (all platforms)

Function Description
Session.tryGet model Get session if authenticated
Session.isAuthenticated model Check auth status
Session.tryGetAccessToken model Get access token

Messages

Message Trigger
LogIn User initiates login — redirects to IdP
LogOut User initiates logout — clears session

Project structure

Shared source files live in src/ and are symlinked into platform-specific directories (netstandard/, reactnative/). This ensures Fable's NuGet package cracker resolves sources correctly when consuming packages.

src/              — shared sources (types, crypto, discovery, token, etc.)
netstandard/      — .NET platform (symlinks to src/ + dotnet-specific files)
reactnative/      — React Native platform (symlinks to src/ + RN-specific files)

Symlinks are checked into git (via .gitattributes). On a fresh clone they should work on macOS/Linux. On Windows, enable Developer Mode or run git config core.symlinks true before cloning.

About

OIDC client for fable elmish apps

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors