Skip to content

Commit

Permalink
Merge pull request #1 from Sovietaced/initial
Browse files Browse the repository at this point in the history
Initial implementation
  • Loading branch information
Sovietaced authored Jan 2, 2024
2 parents 3c59327 + b1fb6ab commit cd7b4fb
Show file tree
Hide file tree
Showing 18 changed files with 1,202 additions and 1 deletion.
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
- package-ecosystem: "gomod" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "daily"
28 changes: 28 additions & 0 deletions .github/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name-template: v$NEXT_PATCH_VERSION
tag-template: v$NEXT_PATCH_VERSION
template: |
# What's Changed
$CHANGES
categories:
- title: ⚠️ Breaking Changes
labels:
- 'breaking change'
- title: 🔒 Security
labels:
- 'security'
- title: 🚀 Features
labels:
- 'enhancement'
- 'feature'
- title: 🐛 Bug Fixes
labels:
- 'bug'
- title: 📖 Documentation
labels:
- 'documentation'
- title: 🧹 Housekeeping
labels:
- 'chore'
- 'test flakiness'
- title: 📦 Dependency updates
label: 'dependencies'
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
on: [push, pull_request]
name: ci
jobs:
test:
strategy:
matrix:
go-version: [1.21.x]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- run: go test ./...
41 changes: 41 additions & 0 deletions .github/workflows/release-drafter.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Release Drafter

on:
push:
# branches to consider in the event; optional, defaults to all
branches:
- main
# pull_request event is required only for autolabeler
pull_request:
# Only following types are handled by the action, but one can default to all as well
types: [opened, reopened, synchronize]
# pull_request_target event is required for autolabeler to support PRs from forks
# pull_request_target:
# types: [opened, reopened, synchronize]

permissions:
contents: read

jobs:
update_release_draft:
permissions:
# write permission is required to create a github release
contents: write
# write permission is required for autolabeler
# otherwise, read permission is required at least
pull-requests: write
runs-on: ubuntu-latest
steps:
# (Optional) GitHub Enterprise requires GHE_HOST variable set
#- name: Set GHE_HOST
# run: |
# echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV

# Drafts your next Release notes as Pull Requests are merged into "master"
- uses: release-drafter/release-drafter@v5
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
# with:
# config-name: my-config.yml
# disable-autolabeler: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@

# Go workspace file
go.work

# IntelliJ
.idea/*
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
# okta-jwt-verifier
# okta-jwt-verifier

[![Test](https://github.com/sovietaced/okta-jwt-verifier/actions/workflows/ci.yml/badge.svg)](https://github.com/sovietaced/okta-jwt-verifier/actions/workflows/ci.yml)
[![GoDoc](https://godoc.org/github.com/sovietaced/okta-jwt-verifier?status.png)](http://godoc.org/github.com/sovietaced/okta-jwt-verifier)
[![Go Report](https://goreportcard.com/badge/github.com/sovietaced/okta-jwt-verifier)](https://goreportcard.com/report/github.com/sovietaced/okta-jwt-verifier)

Alternative implementation to the official [okta-jwt-verifier](https://github.com/okta/okta-jwt-verifier-golang) that
includes support for telemetry (ie. OpenTelemetry), minimizing operational latency, and testability.
26 changes: 26 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module github.com/sovietaced/okta-jwt-verifier

go 1.21.5

require (
github.com/MicahParks/jwkset v0.5.4
github.com/MicahParks/keyfunc/v3 v3.1.1
github.com/benbjohnson/clock v1.3.5
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/stretchr/testify v1.8.4
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1
go.opentelemetry.io/otel v1.21.0
go.opentelemetry.io/otel/sdk v1.21.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
golang.org/x/sys v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
39 changes: 39 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
github.com/MicahParks/jwkset v0.5.4 h1:59s9OUNIKF3g+IXYm3pa4vPXXEudRNetyy3+H6KpKdw=
github.com/MicahParks/jwkset v0.5.4/go.mod h1:fOx7dCX+XgPDzcRbZzi9DMY3vyebWXmsz7XPqstr3ms=
github.com/MicahParks/keyfunc/v3 v3.1.1 h1:ghC5jcuU4/TTQQ9Ns7TEVuhnscQOH+WL4//Jmsy5/DA=
github.com/MicahParks/keyfunc/v3 v3.1.1/go.mod h1:Qmrhb9tkHX1i/kCiLAPDOCWIEfN9yq7u/tkP16lmLL8=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
12 changes: 12 additions & 0 deletions keyfunc/keyfunc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package keyfunc

import (
"context"
"github.com/golang-jwt/jwt/v5"
)

// Provider is a pluggable provider of JWT verifying key functions.
type Provider interface {
// GetKeyfunc gets the JWT verifying key function for an issuer.
GetKeyfunc(ctx context.Context) (jwt.Keyfunc, error)
}
157 changes: 157 additions & 0 deletions keyfunc/okta/okta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package okta

import (
"context"
"fmt"
"github.com/MicahParks/keyfunc/v3"
"github.com/benbjohnson/clock"
"github.com/golang-jwt/jwt/v5"
"github.com/sovietaced/okta-jwt-verifier/metadata"
"io"
"net/http"
"sync"
"time"
)

// Options are configurable options for the KeyfuncProvider.
type Options struct {
httpClient *http.Client
clock clock.Clock
cacheTtl time.Duration
}

// WithHttpClient allows for a configurable http client.
func WithHttpClient(httpClient *http.Client) Option {
return func(mo *Options) {
mo.httpClient = httpClient
}
}

func withClock(clock clock.Clock) Option {
return func(mo *Options) {
mo.clock = clock
}
}

// WithCacheTtl specifies the TTL on the Okta JWK set.
func WithCacheTtl(ttl time.Duration) Option {
return func(mo *Options) {
mo.cacheTtl = ttl
}
}

func defaultOptions() *Options {
opts := &Options{}
WithHttpClient(http.DefaultClient)(opts)
withClock(clock.New())(opts)
WithCacheTtl(5 * time.Minute)(opts)
return opts
}

// Option for the KeyfuncProvider
type Option func(*Options)

type cachedKeyfunc struct {
expiration time.Time
keyfunc jwt.Keyfunc
}

func newCachedKeyfunc(expiration time.Time, keyfunc jwt.Keyfunc) *cachedKeyfunc {
return &cachedKeyfunc{expiration: expiration, keyfunc: keyfunc}
}

// KeyfuncProvider implements the keyfunc.KeyfuncProvider and generates JWT validating functions for Okta tokens.
type KeyfuncProvider struct {
mp metadata.Provider
httpClient *http.Client
clock clock.Clock

keyfuncMutex sync.Mutex
cacheTtl time.Duration
cachedKeyfunc *cachedKeyfunc
}

// NewKeyfuncProvider creates a new KeyfuncProvider.
func NewKeyfuncProvider(mp metadata.Provider, options ...Option) *KeyfuncProvider {
opts := defaultOptions()
for _, option := range options {
option(opts)
}

return &KeyfuncProvider{mp: mp, httpClient: opts.httpClient, clock: opts.clock, cacheTtl: opts.cacheTtl}
}

// GetKeyfunc gets a jwt.Keyfunc based on the OIDC metadata.
func (kp *KeyfuncProvider) GetKeyfunc(ctx context.Context) (jwt.Keyfunc, error) {
md, err := kp.mp.GetMetadata(ctx)
if err != nil {
return nil, fmt.Errorf("getting metadata: %w", err)
}

keyfunc, err := kp.getOrFetchKeyfunc(ctx, md.JwksUri)
if err != nil {
return nil, fmt.Errorf("getting or fetching keyfunc: %w", err)
}

return keyfunc, nil
}

func (kp *KeyfuncProvider) getOrFetchKeyfunc(ctx context.Context, jwksUri string) (jwt.Keyfunc, error) {
cachedKeyfuncCopy := kp.cachedKeyfunc

if cachedKeyfuncCopy != nil && kp.clock.Now().Before(cachedKeyfuncCopy.expiration) {
return cachedKeyfuncCopy.keyfunc, nil
}

// Acquire a lock
kp.keyfuncMutex.Lock()
defer kp.keyfuncMutex.Unlock()

// Check again to protect against races
cachedKeyfuncCopy = kp.cachedKeyfunc

if cachedKeyfuncCopy != nil && kp.clock.Now().Before(cachedKeyfuncCopy.expiration) {
return cachedKeyfuncCopy.keyfunc, nil
}

keyfunc, err := kp.fetchKeyfunc(ctx, jwksUri)
if err != nil {
return nil, fmt.Errorf("fetching keyfunc: %w", err)
}

expiration := kp.clock.Now().Add(kp.cacheTtl)
kp.cachedKeyfunc = newCachedKeyfunc(expiration, keyfunc)

return keyfunc, nil
}

func (kp *KeyfuncProvider) fetchKeyfunc(ctx context.Context, jwksUri string) (jwt.Keyfunc, error) {

httpRequest, err := http.NewRequestWithContext(ctx, http.MethodGet, jwksUri, nil)
if err != nil {
return nil, fmt.Errorf("creating new http request: %w", err)
}
resp, err := kp.httpClient.Do(httpRequest)
if err != nil {
return nil, fmt.Errorf("making http request for jwks: %w", err)
}
defer resp.Body.Close()

ok := resp.StatusCode >= 200 && resp.StatusCode < 300
if !ok {
return nil, fmt.Errorf("request for jwks %q was not HTTP 2xx OK, it was: %d", jwksUri, resp.StatusCode)
}

jwkJson, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read jwks response body: %w", err)
}

kf, err := keyfunc.NewJWKSetJSON(jwkJson)
if err != nil {
return nil, fmt.Errorf("failed to create keyfunc from jwk json: %w", err)
}

return kf.Keyfunc, nil

}
Loading

0 comments on commit cd7b4fb

Please sign in to comment.