diff --git a/CHANGELOG.md b/CHANGELOG.md index 1490f71a..07e2476b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 2493fec4..bd424dbb 100644 --- a/README.md +++ b/README.md @@ -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
Convert the following `curl` command into a token request using this gem... @@ -61,6 +61,61 @@ NOTE: `header` - The content type specified in the `curl` is already the default
+
+Complete E2E single file script against [navikt/mock-oauth2-server](https://github.com/navikt/mock-oauth2-server) + +- 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//.well-known/openid-configuration, where 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) + +
+ If it seems like you are in the wrong place, you might try one of these: * [OAuth 2.0 Spec][oauth2-spec] diff --git a/config-ssl.json b/config-ssl.json index f0a8da2e..f906d9e2 100644 --- a/config-ssl.json +++ b/config-ssl.json @@ -1,7 +1,21 @@ { "interactiveLogin": true, "httpServer": { - "type": "NettyWrapper", - "ssl": {} - } -} \ No newline at end of file + "type": "NettyWrapper" + }, + "tokenCallbacks": [ + { + "issuerId": "default", + "requestMappings": [ + { + "requestParam": "grant_type", + "match": "client_credentials", + "claims": { + "sub": "demo-sub", + "aud": ["demo-aud"] + } + } + ] + } + ] +} diff --git a/docker-compose-ssl.yml b/docker-compose-ssl.yml index 9a17fbba..f482d41d 100644 --- a/docker-compose-ssl.yml +++ b/docker-compose-ssl.yml @@ -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: diff --git a/examples/e2e.rb b/examples/e2e.rb new file mode 100644 index 00000000..22329c1a --- /dev/null +++ b/examples/e2e.rb @@ -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