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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,21 @@ Please file a bug if you notice a violation of semantic versioning.

## [Unreleased]
### Added
- E2E example using mock test server added in v2.0.11
- mock-oauth2-server upgraded to v2.3.0
- https://github.com/navikt/mock-oauth2-server
- `docker compose -f docker-compose-ssl.yml up -d --wait`
- `ruby examples/e2e.rb`
- `docker compose -f docker-compose-ssl.yml down`
- mock server readiness wait is 90s
- override via E2E_WAIT_TIMEOUT
- Apache SkyWalking Eyes dependency license check
### Changed
- Many improvements to make CI more resilient (past/future proof)
### Deprecated
### Removed
### Fixed

### Security

## [2.0.15] - 2025-09-08
Expand Down
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ OAuth 2.0 focuses on client developer simplicity while providing specific author
desktop applications, mobile phones, and living room devices.
This is a RubyGem for implementing OAuth 2.0 clients (not servers) in Ruby applications.

### Quick Example
### Quick Examples

<details>
<summary>Convert the following `curl` command into a token request using this gem...</summary>
Expand Down Expand Up @@ -61,6 +61,61 @@ NOTE: `header` - The content type specified in the `curl` is already the default

</details>

<details>
<summary>Complete E2E single file script against [navikt/mock-oauth2-server](https://github.com/navikt/mock-oauth2-server)</summary>

- E2E example using the mock test server added in v2.0.11

```console
docker compose -f docker-compose-ssl.yml up -d --wait
ruby examples/e2e.rb
# If your machine is slow or Docker pulls are cold, increase the wait:
E2E_WAIT_TIMEOUT=120 ruby examples/e2e.rb
# The mock server serves HTTP on 8080; the example points to http://localhost:8080 by default.
```

The output should be something like this:

```console
➜ ruby examples/e2e.rb
Access token (truncated): eyJraWQiOiJkZWZhdWx0...
userinfo status: 200
userinfo body: {"sub" => "demo-sub", "aud" => ["demo-aud"], "nbf" => 1757816758000, "iss" => "http://localhost:8080/default", "exp" => 1757820358000, "iat" => 1757816758000, "jti" => "d63b97a7-ebe5-4dea-93e6-d542caba6104"}
E2E complete
```

Make sure to shut down the mock server when you are done:

```console
docker compose -f docker-compose-ssl.yml down
```

Troubleshooting: validate connectivity to the mock server

- Check container status and port mapping:
- docker compose -f docker-compose-ssl.yml ps
- From the host, try the discovery URL directly (this is what the example uses by default):
- curl -v http://localhost:8080/default/.well-known/openid-configuration
- If that fails immediately, also try: curl -v --connect-timeout 2 http://127.0.0.1:8080/default/.well-known/openid-configuration
- From inside the container (to distinguish container vs host networking):
- docker exec -it oauth2-mock-oauth2-server-1 curl -v http://127.0.0.1:8080/default/.well-known/openid-configuration
- Simple TCP probe from the host:
- nc -vz localhost 8080 # or: ruby -rsocket -e 'TCPSocket.new("localhost",8080).close; puts "tcp ok"'
- Inspect which host port 8080 is bound to (should be 8080):
- docker inspect -f '{{ (index (index .NetworkSettings.Ports "8080/tcp") 0).HostPort }}' oauth2-mock-oauth2-server-1
- Look at server logs for readiness/errors:
- docker logs -n 200 oauth2-mock-oauth2-server-1
- On Linux, ensure nothing else is bound to 8080 and that firewall/SELinux aren’t blocking:
- ss -ltnp | grep :8080

Notes
- Discovery URL pattern is: http://localhost:8080/<realm>/.well-known/openid-configuration, where <realm> defaults to "default".
- You can change these with env vars when running the example:
- E2E_ISSUER_BASE (default: http://localhost:8080)
- E2E_REALM (default: default)

</details>

If it seems like you are in the wrong place, you might try one of these:

* [OAuth 2.0 Spec][oauth2-spec]
Expand Down
22 changes: 18 additions & 4 deletions config-ssl.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
{
"interactiveLogin": true,
"httpServer": {
"type": "NettyWrapper",
"ssl": {}
}
}
"type": "NettyWrapper"
},
"tokenCallbacks": [
{
"issuerId": "default",
"requestMappings": [
{
"requestParam": "grant_type",
"match": "client_credentials",
"claims": {
"sub": "demo-sub",
"aud": ["demo-aud"]
}
}
]
}
]
}
4 changes: 2 additions & 2 deletions docker-compose-ssl.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
services:
mock-oauth2-server:
image: ghcr.io/navikt/mock-oauth2-server:2.1.11
image: ghcr.io/navikt/mock-oauth2-server:2.3.0
restart: unless-stopped
ports:
- "8080:8080"
hostname: host.docker.internal
volumes:
- ./config-ssl.json:/app/config.json:Z
environment:
Expand Down
138 changes: 138 additions & 0 deletions examples/e2e.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# frozen_string_literal: true

# End-to-end example using oauth2 gem against a local mock-oauth2-server.
# Prerequisites:
# 1) Start the mock server (HTTP on 8080):
# docker compose -f docker-compose-ssl.yml up -d --wait
# 2) Run this script:
# ruby examples/e2e.rb
# 3) Stop the server when you're done:
# docker compose -f docker-compose-ssl.yml down
# Notes:
# - The mock server uses a self-signed certificate. SSL verification is disabled in this example.
# - Tested down to Ruby 2.4 (avoid newer syntax).

require "oauth2"
require "json"
require "net/http"
require "uri"

module E2E
class ClientCredentialsDemo
attr_reader :client_id, :client_secret, :issuer_base, :realm

# issuer_base: e.g., https://localhost:8080
# realm: mock-oauth2-server issuer id ("default" by default)
def initialize(client_id, client_secret, issuer_base, realm)
@client_id = client_id
@client_secret = client_secret
@issuer_base = issuer_base
@realm = realm
end

def run
wait_for_server_ready
well_known = discover
token = fetch_token(well_known)
puts "Access token (truncated): #{token.token[0, 20]}..."
call_userinfo(well_known, token)
puts "E2E complete"
end

private

def discovery_url
File.join(@issuer_base, @realm, "/.well-known/openid-configuration")
end

def wait_for_server_ready(timeout = nil)
timeout = (timeout || ENV["E2E_WAIT_TIMEOUT"] || 90).to_i
uri = URI(discovery_url)
deadline = Time.now + timeout
announced = false
loop do
begin
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
req = Net::HTTP::Get.new(uri.request_uri)
res = http.request(req)
return if res.code.to_i == 200
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET, SocketError, EOFError, OpenSSL::SSL::SSLError
# ignore and retry until timeout
end
unless announced
puts "Waiting for mock OAuth2 server at #{uri} ..."
announced = true
end
break if Time.now >= deadline
sleep(0.5)
end
raise "Server not reachable at #{uri} within #{timeout}s. Ensure it's running: docker compose -f docker-compose-ssl.yml up -d --wait. You can increase the wait by setting E2E_WAIT_TIMEOUT (seconds)."
end

def discover
uri = URI(discovery_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
req = Net::HTTP::Get.new(uri.request_uri)
res = http.request(req)
unless res.code.to_i == 200
raise "Discovery failed: #{res.code} #{res.message} - #{res.body}"
end
data = JSON.parse(res.body)
# Expect token_endpoint and possibly userinfo_endpoint
data
end

def fetch_token(well_known)
client = OAuth2::Client.new(
@client_id,
@client_secret,
site: @issuer_base,
token_url: URI.parse(well_known["token_endpoint"]).request_uri,
ssl: {verify: false},
auth_scheme: :request_body, # send client creds in request body (compatible default for mock servers)
)
# Use client_credentials grant
client.client_credentials.get_token
end

def call_userinfo(well_known, token)
userinfo = well_known["userinfo_endpoint"]
unless userinfo
puts "No userinfo_endpoint advertised by server; skipping userinfo call."
return
end
uri = URI(userinfo)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == "https"
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
req = Net::HTTP::Get.new(uri.request_uri)
req["Authorization"] = "Bearer #{token.token}"
res = http.request(req)
puts "userinfo status: #{res.code} #{res.message}"
if res.code.to_i == 200
begin
body = JSON.parse(res.body)
rescue StandardError
body = res.body
end
puts "userinfo body: #{body.inspect}"
else
puts "userinfo error body: #{res.body}"
end
end
end
end

if __FILE__ == $PROGRAM_NAME
# These must match the mock server configuration (see config-ssl.json)
client_id = ENV["E2E_CLIENT_ID"] || "demo-client"
client_secret = ENV["E2E_CLIENT_SECRET"] || "demo-secret"
issuer_base = ENV["E2E_ISSUER_BASE"] || "http://localhost:8080"
realm = ENV["E2E_REALM"] || "default"

E2E::ClientCredentialsDemo.new(client_id, client_secret, issuer_base, realm).run
end
Loading