Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ jobs:
fail-fast: false
matrix:
version:
- '1.0'
- '1.10' # minimum supported Julia (required by HTTP v2 / Reseau)
- '1' # automatically expands to the latest stable 1.x release of Julia
- nightly
os:
- ubuntu-latest
arch:
# Reseau (HTTP v2 TLS backend) does not support 32-bit; x64 only.
- x64
- x86
include:
# test macOS and Windows with latest Julia only
- os: macOS-latest
Expand All @@ -28,9 +28,6 @@ jobs:
- os: windows-latest
arch: x64
version: 1
- os: windows-latest
arch: x86
version: 1
steps:
- uses: actions/checkout@v4
- uses: julia-actions/setup-julia@v2
Expand Down
15 changes: 8 additions & 7 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
name = "OpenIDConnect"
uuid = "195a77af-17c0-520f-ac58-3336c7ea8576"
keywords = ["openidconnect", "openid-connect", "openid", "openidc", "julia", "identity"]
authors = ["Tanmay Mohapatra <tanmaykm@gmail.com>"]
license = "MIT"
desc = "OpenID Connect for Julia"
version = "0.1.8"
version = "0.2.0"
authors = ["Tanmay Mohapatra <tanmaykm@gmail.com>"]

[deps]
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
JWTs = "d850fbd6-035d-5a70-a269-1ca2e636ac6c"
MbedTLS = "739be429-bea8-5141-9913-cc70e7f3736d"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Reseau = "802f3686-a58f-41ce-bb0c-3c43c75bba36"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"

[compat]
julia = "1"
HTTP = "0.8, 0.9, 1"
HTTP = "2.1.1"
JSON = "0.21, 1"
JWTs = "0.1, 0.2, 0.3"
MbedTLS = "0.6.8, 0.7, 1"
Reseau = "1.2.1"
SHA = "<0.0.1, 0.7, 1"
julia = "1.10"

[extras]
NetworkOptions = "ca575930-c2e3-43a9-ace4-1e988b2c1908"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "Random"]
test = ["Test", "Random", "NetworkOptions"]
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function OIDCCtx(
client_secret::String,
scopes::Vector{String}=DEFAULT_SCOPES;
verify::Union{Nothing,Bool}=nothing,
cacrt::Union{Nothing,String,MbedTLS.CRT}=nothing,
cacrt::Union{Nothing,String}=nothing,
state_timeout_secs::Int=DEFAULT_STATE_TIMEOUT_SECS,
allowed_skew_secs::Int=DEFAULT_SKEW_SECS,
key_refresh_secs::Int=DEFAULT_KEY_REFRESH_SECS),
Expand All @@ -34,7 +34,7 @@ Parameters:

Keyword Parameters:
- `verify`: whether to validate the server certificate
- `cacrt`: the CA certificate to use to check the server certificate
- `cacrt`: path to a CA certificate file (PEM) to use to check the server certificate. Must be a path to an existing file (since the HTTP v2 / Reseau migration, passing certificate contents or an `MbedTLS.CRT` is no longer supported)
- `state_timeout_secs`: seconds for which to keep the state associated with an authorization request (default: 60 seconds), server responses beyond this are rejected as stale
- `allowed_skew_secs`: while validating tokens, seconds to allow to account for time skew between machines (default: 120 seconds)
- `key_refresh_secs`: time interval in which to refresh the JWT signing keys (default: 1hr)
Expand Down
50 changes: 37 additions & 13 deletions src/OpenIDConnect.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ module OpenIDConnect

using HTTP
using JSON
using MbedTLS
using Base64
using Random
using SHA
using SHA
using JWTs

# Import Reseau.TLS for HTTP v2 custom certificate support
import Reseau
const TLS = Reseau.TLS

const DEFAULT_SCOPES = ["openid", "profile", "email"]
const DEFAULT_STATE_TIMEOUT_SECS = 60
const DEFAULT_SKEW_SECS = 2*60
Expand Down Expand Up @@ -37,25 +40,46 @@ struct OIDCCtx
random_device::RandomDevice

function OIDCCtx(issuer::String, redirect_uri::String, client_id::String, client_secret::String, scopes::Vector{String}=DEFAULT_SCOPES;
verify::Union{Nothing,Bool}=nothing, cacrt::Union{Nothing,String,MbedTLS.CRT}=nothing,
verify::Union{Nothing,Bool}=nothing, cacrt::Union{Nothing,String}=nothing,
state_timeout_secs::Int=DEFAULT_STATE_TIMEOUT_SECS, allowed_skew_secs::Int=DEFAULT_SKEW_SECS, key_refresh_secs::Int=DEFAULT_KEY_REFRESH_SECS,
random_device::RandomDevice=RandomDevice())
endswith(issuer, "/") || (issuer = issuer * "/")
openid_config_url = issuer * ".well-known/openid-configuration"
http_tls_opts = Dict{Symbol,Any}()
http_tls_opts[:socket_type_tls] = MbedTLS.SSLContext

if verify !== nothing
http_tls_opts[:require_ssl_verification] = verify
end

if cacrt !== nothing
if isa(cacrt, String)
cacrt = isfile(cacrt) ? MbedTLS.crt_parse_file(cacrt) : MbedTLS.crt_parse(cacrt)
if !isfile(cacrt)
error("cacrt must be a path to an existing certificate file; got: $cacrt")
end
conf = MbedTLS.SSLConfig(verify === nothing || verify)
MbedTLS.ca_chain!(conf, cacrt)
http_tls_opts[:sslconfig] = conf
# A custom CA requires a custom HTTP.Client built around a Reseau TLS.Config.
# The verify intent is baked into the TLS.Config below (verify_peer/verify_hostname);
# we must NOT also pass require_ssl_verification, since HTTP v2 throws when that
# keyword is combined with an explicit client.
verify_peer = verify === nothing || verify
tls_config = TLS.Config(
nothing, # server_name
verify_peer, # verify_peer
verify_peer, # verify_hostname
TLS.ClientAuthMode.NoClientCert, # client_auth
nothing, # cert_file
nothing, # key_file
cacrt, # ca_file
nothing, # client_ca_file
String[], # alpn_protocols
UInt16[], # curve_preferences
Int64(30_000_000_000), # handshake_timeout_ns (30 seconds)
TLS.TLS1_2_VERSION, # min_version
nothing, # max_version
false, # session_tickets_disabled
64, # session_cache_capacity
)
# Create custom transport with TLS config
transport = HTTP.Transport(; tls_config)
# Create custom client
http_tls_opts[:client] = HTTP.Client(; transport)
elseif verify !== nothing
# No custom CA: control verification through the per-request keyword on the default client.
http_tls_opts[:require_ssl_verification] = verify
end

# fetch and store the openid config, along with the additional args for SSL
Expand Down
22 changes: 22 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ using OpenIDConnect
using Test
using Random
using HTTP
using NetworkOptions

function test_state_store()
@testset "State store" begin
Expand Down Expand Up @@ -68,7 +69,28 @@ function test_oidc_flow()
end
end

function test_custom_cacrt()
@testset "Custom CA certificate" begin
cafile = NetworkOptions.ca_roots_path()

# A custom CA file builds a custom HTTP.Client; the context must construct
# and resolve the openid configuration successfully.
ctx = OIDCCtx("https://accounts.google.com", "http://127.0.0.1:8888/auth/login", "test_client_id", "test_client_secret"; cacrt=cafile)
@test OpenIDConnect.token_endpoint(ctx) == "https://oauth2.googleapis.com/token"

# Regression: verify=false together with cacrt must not throw. HTTP v2 rejects
# require_ssl_verification overrides when an explicit client is passed, so the
# verify intent has to be carried by the TLS config instead.
ctx = OIDCCtx("https://accounts.google.com", "http://127.0.0.1:8888/auth/login", "test_client_id", "test_client_secret"; verify=false, cacrt=cafile)
@test OpenIDConnect.token_endpoint(ctx) == "https://oauth2.googleapis.com/token"

# A cacrt that is not an existing file is rejected with a clear error.
@test_throws ErrorException OIDCCtx("https://accounts.google.com", "http://127.0.0.1:8888/auth/login", "test_client_id", "test_client_secret"; cacrt="/no/such/ca-file.pem")
end
end

@testset "OpenIDConnect" begin
test_state_store()
test_oidc_flow()
test_custom_cacrt()
end
Loading