diff --git a/.gitignore b/.gitignore index 3fc25bd96..3f1d9c02a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ config/database.yml .rvmrc_custom .rvmrc.local config/oidc_key.pem +config/jwks/ # Mailing list stuff config/email_offset diff --git a/app/controllers/api/openid_connect/clients_controller.rb b/app/controllers/api/openid_connect/clients_controller.rb index ae906c58a..d1f56dabe 100644 --- a/app/controllers/api/openid_connect/clients_controller.rb +++ b/app/controllers/api/openid_connect/clients_controller.rb @@ -32,7 +32,7 @@ module Api private def http_error_page_as_json(e) - render json: { error: :invalid_request, error_description: e.message}, status: 400 + render json: {error: :invalid_request, error_description: e.message}, status: 400 end def validation_fail_as_json(e) diff --git a/app/controllers/api/openid_connect/discovery_controller.rb b/app/controllers/api/openid_connect/discovery_controller.rb index 8b4602338..5a12c6007 100644 --- a/app/controllers/api/openid_connect/discovery_controller.rb +++ b/app/controllers/api/openid_connect/discovery_controller.rb @@ -25,7 +25,7 @@ module Api request_object_signing_alg_values_supported: %i(HS256 HS384 HS512), subject_types_supported: %w(public pairwise), id_token_signing_alg_values_supported: %i(RS256), - token_endpoint_auth_methods_supported: %w(client_secret_basic client_secret_post), + token_endpoint_auth_methods_supported: %w(client_secret_basic client_secret_post private_key_jwt), claims_supported: %w(sub nickname profile picture) ) end diff --git a/app/controllers/api/openid_connect/token_endpoint_controller.rb b/app/controllers/api/openid_connect/token_endpoint_controller.rb new file mode 100644 index 000000000..86d3d2f79 --- /dev/null +++ b/app/controllers/api/openid_connect/token_endpoint_controller.rb @@ -0,0 +1,64 @@ +module Api + module OpenidConnect + class TokenEndpointController < ApplicationController + def create + req = Rack::Request.new(request.env) + if req["client_assertion_type"] == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + handle_jwt_bearer(req) + end + self.status, self.response.headers, self.response_body = Api::OpenidConnect::TokenEndpoint.new.call(request.env) + nil + end + + private + + def handle_jwt_bearer(req) + jwt_string = req["client_assertion"] + jwt = JSON::JWT.decode jwt_string, :skip_verification + o_auth_app = Api::OpenidConnect::OAuthApplication.find_by(client_id: jwt["iss"]) + raise Rack::OAuth2::Server::Authorize::BadRequest(:invalid_request) unless o_auth_app + public_key = fetch_public_key(o_auth_app, jwt) + JSON::JWT.decode(jwt_string, JSON::JWK.new(public_key).to_key) + req.update_param("client_id", o_auth_app.client_id) + req.update_param("client_secret", o_auth_app.client_secret) + end + + def fetch_public_key(o_auth_app, jwt) + jwks_file_path = File.join(Rails.root, "config", "jwks", o_auth_app.jwks_file) + public_key = fetch_public_key_from_json(File.read(jwks_file_path), jwt) + if public_key.empty? && o_auth_app.jwks_uri + uri = URI.parse(o_auth_app.jwks_uri) + response = Net::HTTP.get_response(uri) + File.write jwks_file_path, response.body + public_key = fetch_public_key_from_json(response.body, jwt) + end + raise Rack::OAuth2::Server::Authorize::BadRequest(:unauthorized_client) if public_key.empty? + public_key + end + + def fetch_public_key_from_json(string, jwt) + json = JSON.parse(string) + keys = json["keys"] + public_key = get_key_from_kid(keys, jwt.header["kid"]) + public_key + end + + def get_key_from_kid(keys, kid) + keys.each do |key| + return key if key.has_value?(kid) + end + end + + rescue_from Rack::OAuth2::Server::Authorize::BadRequest, JSON::JWT::InvalidFormat do |e| + logger.info e.backtrace[0, 10].join("\n") + render json: {error: :invalid_request, error_description: e.message, status: e.status} + end + rescue_from JSON::JWT::InvalidFormat do |e| + render json: {error: :invalid_request, error_description: e.message, status: 400} + end + rescue_from JSON::JWT::VerificationFailed do |e| + render json: {error: :invalid_grant, error_description: e.message, status: 400} + end + end + end +end diff --git a/app/models/api/openid_connect/o_auth_application.rb b/app/models/api/openid_connect/o_auth_application.rb index 668592dab..a89fa0c05 100644 --- a/app/models/api/openid_connect/o_auth_application.rb +++ b/app/models/api/openid_connect/o_auth_application.rb @@ -1,3 +1,5 @@ +require "digest" + module Api module OpenidConnect class OAuthApplication < ActiveRecord::Base @@ -68,7 +70,7 @@ module Api def supported_metadata %i(client_name response_types grant_types application_type contacts logo_uri client_uri policy_uri tos_uri redirect_uris - sector_identifier_uri subject_type) + sector_identifier_uri subject_type token_endpoint_auth_method jwks jwks_uri) end def registrar_attributes(registrar) @@ -77,11 +79,30 @@ module Api next unless value if key == :subject_type attr[:ppid] = (value == "pairwise") + elsif key == :jwks_uri + uri = URI.parse(value) + response = Net::HTTP.get_response(uri) + file_name = create_file_path(response.body) + attr[:jwks_file] = file_name + ".json" + attr[:jwks_uri] = value + elsif key == :jwks + file_name = create_file_path(value.to_json) + attr[:jwks_file] = file_name + ".json" else attr[key] = value end end end + + def create_file_path(content) + file_name = Base64.urlsafe_encode64(Digest::SHA256.base64digest(content)) + directory_name = File.join(Rails.root, "config", "jwks") + Dir.mkdir(directory_name) unless File.exist?(directory_name) + jwk_file_path = File.join(Rails.root, "config", "jwks", file_name + ".json") + File.write jwk_file_path, content + File.chmod(0600, jwk_file_path) + file_name + end end end end diff --git a/config/routes.rb b/config/routes.rb index beed890c6..218c48911 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -242,7 +242,7 @@ Diaspora::Application.routes.draw do resources :clients, only: :create get "clients/find", to: "clients#find" - post "access_tokens", to: proc {|env| Api::OpenidConnect::TokenEndpoint.new.call(env) } + post "access_tokens", to: "token_endpoint#create" # Authorization Servers MUST support the use of the HTTP GET and POST methods at the Authorization Endpoint # See http://openid.net/specs/openid-connect-core-1_0.html#AuthResponseValidation diff --git a/db/migrate/20150613202109_create_o_auth_applications.rb b/db/migrate/20150613202109_create_o_auth_applications.rb index 1e7cf290f..4b4db75f2 100644 --- a/db/migrate/20150613202109_create_o_auth_applications.rb +++ b/db/migrate/20150613202109_create_o_auth_applications.rb @@ -16,6 +16,10 @@ class CreateOAuthApplications < ActiveRecord::Migration t.string :policy_uri t.string :tos_uri t.string :sector_identifier_uri + t.string :token_endpoint_auth_method + t.string :jwks_uri + t.string :jwks_file + t.boolean :ppid, default: false t.timestamps null: false diff --git a/db/schema.rb b/db/schema.rb index 52b823bdd..8fce36389 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -273,23 +273,26 @@ ActiveRecord::Schema.define(version: 20150828132451) do add_index "o_auth_access_tokens", ["token"], name: "index_o_auth_access_tokens_on_token", unique: true, length: {"token"=>191}, using: :btree create_table "o_auth_applications", force: :cascade do |t| - t.integer "user_id", limit: 4 - t.string "client_id", limit: 255 - t.string "client_secret", limit: 255 - t.string "client_name", limit: 255 - t.string "redirect_uris", limit: 255 - t.string "response_types", limit: 255 - t.string "grant_types", limit: 255 - t.string "application_type", limit: 255, default: "web" - t.string "contacts", limit: 255 - t.string "logo_uri", limit: 255 - t.string "client_uri", limit: 255 - t.string "policy_uri", limit: 255 - t.string "tos_uri", limit: 255 - t.string "sector_identifier_uri", limit: 255 - t.boolean "ppid", default: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.integer "user_id", limit: 4 + t.string "client_id", limit: 255 + t.string "client_secret", limit: 255 + t.string "client_name", limit: 255 + t.string "redirect_uris", limit: 255 + t.string "response_types", limit: 255 + t.string "grant_types", limit: 255 + t.string "application_type", limit: 255, default: "web" + t.string "contacts", limit: 255 + t.string "logo_uri", limit: 255 + t.string "client_uri", limit: 255 + t.string "policy_uri", limit: 255 + t.string "tos_uri", limit: 255 + t.string "sector_identifier_uri", limit: 255 + t.string "token_endpoint_auth_method", limit: 255 + t.string "jwks_uri", limit: 255 + t.string "jwks_file", limit: 255 + t.boolean "ppid", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "o_auth_applications", ["client_id"], name: "index_o_auth_applications_on_client_id", unique: true, length: {"client_id"=>191}, using: :btree diff --git a/spec/controllers/api/openid_connect/clients_controller_spec.rb b/spec/controllers/api/openid_connect/clients_controller_spec.rb index 1b138386a..d124ff042 100644 --- a/spec/controllers/api/openid_connect/clients_controller_spec.rb +++ b/spec/controllers/api/openid_connect/clients_controller_spec.rb @@ -19,6 +19,87 @@ describe Api::OpenidConnect::ClientsController, type: :controller do end end + context "when valid parameters with jwks is passed" do + it "should return a client id" do + stub_request(:get, "http://example.com/uris") + .with(headers: {"Accept" => "*/*", "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "Host" => "example.com", "User-Agent" => "Ruby"}) + .to_return(status: 200, body: "[\"http://localhost\"]", headers: {}) + post :create, redirect_uris: ["http://localhost"], client_name: "diaspora client", + response_types: [], grant_types: [], application_type: "web", contacts: [], + logo_uri: "http://example.com/logo.png", client_uri: "http://example.com/client", + policy_uri: "http://example.com/policy", tos_uri: "http://example.com/tos", + sector_identifier_uri: "http://example.com/uris", subject_type: "pairwise", + token_endpoint_auth_method: "private_key_jwt", + "jwks": { + "keys": + [ + { + "use": "enc", + "e": "AQAB", + "d": "-lTBWkI-----lvCO6tuiDsR4qgJnUwnndQFwEI_4mLmD3iNWXrc8N--5Cjq55eLtuJjtvuQ", + "n": "--zYRQNDvIVsBDLQQIgrbctuGqj6lrXb31Jj3JIEYqH_4h5X9d0Q", + "q": "1q-r----pFtyTz_JksYYaotc_Z3Zy-Szw6a39IDbuYGy1qL-15oQuc", + "p": "-BfRjdgYouy4c6xAnGDgSMTip1YnPRyvbMaoYT9E_tEcBW5wOeoc", + "kid": "a0", + "kty": "RSA" + }, + {"use": "sig", + "e": "AQAB", + "d": "--x-gW---LRPowKrdvTuTo2p--HMI0pIEeFs7H_u5OW3jihjvoFClGPynHQhgWmQzlQRvWRXh6FhDVqFeGQ", + "n": "---TyeadDqQPWgbqX69UzcGq5irhzN8cpZ_JaTk3Y_uV6owanTZLVvCgdjaAnMYeZhb0KFw", + "q": "5E5XKK5njT--Hx3nF5sne5fleVfU-sZy6Za4B2U75PcE62oZgCPauOTAEm9Xuvrt5aMMovyzR8ecJZhm9bw7naU", + "p": "-BUGA-", + "kid": "a1", + "kty": "RSA"}, + { + "use": "sig", + "crv": "P-256", + "kty": "EC", + "y": "Yg4IRzHBMIsuQK2Oz0Uukp1aNDnpdoyk6QBMtmfGHQQ", + "x": "L0WUeVlc9r6YJd6ie9duvOU1RHwxSkJKA37IK9B4Bpc", + "kid": "a2" + }, + { + "use": "enc", + "crv": "P-256", + "kty": "EC", + "y": "E6E6g5_ziIZvfdAoACctnwOhuQYMvQzA259aftPn59M", + "x": "Yu8_BQE2L0f1MqnK0GumZOaj_77Tx70-LoudyRUnLM4", + "kid": "a3" + } + ] + } + client_json = JSON.parse(response.body) + expect(client_json["client_id"].length).to eq(32) + expect(client_json["ppid"]).to eq(true) + end + end + + context "when valid parameters with jwks_uri is passed" do + it "should return a client id" do + stub_request(:get, "http://example.com/uris") + .with(headers: {:Accept => "*/*", :"Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "Host" => "example.com", :"User-Agent" => "Ruby"}) + .to_return(status: 200, body: "[\"http://localhost\"]", headers: {}) + stub_request(:get, "https://kentshikama.com/api/openid_connect/jwks.json") + .with(headers: {"Accept": "*/*", "Accept-Encoding": "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "Host": "kentshikama.com", "User-Agent": "Ruby"}) + .to_return(status: 200, + body: "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"n\":\"qpW\",\"use\":\"sig\"}]}", headers: {}) + post :create, redirect_uris: ["http://localhost"], client_name: "diaspora client", + response_types: [], grant_types: [], application_type: "web", contacts: [], + logo_uri: "http://example.com/logo.png", client_uri: "http://example.com/client", + policy_uri: "http://example.com/policy", tos_uri: "http://example.com/tos", + sector_identifier_uri: "http://example.com/uris", subject_type: "pairwise", + token_endpoint_auth_method: "private_key_jwt", + jwks_uri: "https://kentshikama.com/api/openid_connect/jwks.json" + client_json = JSON.parse(response.body) + expect(client_json["client_id"].length).to eq(32) + expect(client_json["ppid"]).to eq(true) + end + end + context "when redirect uri is missing" do it "should return a invalid_client_metadata error" do post :create, response_types: [], grant_types: [], application_type: "web", contacts: [], diff --git a/spec/factories.rb b/spec/factories.rb index 0e7493360..7027fd57b 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -328,6 +328,13 @@ FactoryGirl.define do sector_identifier_uri "https://example.com/uri" end + factory :o_auth_application_with_ppid_with_specific_id, class: Api::OpenidConnect::OAuthApplication do + client_name "Diaspora Test Client" + redirect_uris %w(http://localhost:3000/) + ppid true + sector_identifier_uri "https://example.com/uri" + end + factory :o_auth_application_with_multiple_redirects, class: Api::OpenidConnect::OAuthApplication do client_name "Diaspora Test Client" redirect_uris %w(http://localhost:3000/ http://localhost/) diff --git a/spec/fixtures/jwks.json b/spec/fixtures/jwks.json new file mode 100644 index 000000000..be157aeec --- /dev/null +++ b/spec/fixtures/jwks.json @@ -0,0 +1 @@ +{"keys": [{"use": "enc", "e": "AQAB", "d": "lZQv0_81euRLeUYU84Aodh0ar7ymDlzWP5NMra4Jklkb-lTBWkI-u4RMsPqGYyW3KHRoL_pgzZXSzQx8RLQfER6timRWb--NxMMKllZubByU3RqH2ooNuocJurspYiXkznPW1Mg9DaNXL0C2hwWPQHTeUVISpjgi5TCOV1ccWVyksFruya_VNL1CIByB-L0GL1rqbKv32cDwi2A3_jJa61cpzfLSIBe-lvCO6tuiDsR4qgJnUwnndQFwEI_4mLmD3iNWXrc8N-poleV8mBfMqBB5fWwy_ZTFCpmQ5AywGmctaik_wNhMoWuA4tUfY6_1LdKld-5Cjq55eLtuJjtvuQ", "n": "tx3Hjdbc19lkTiohbJrNj4jf2_90MEE122CRrwtFu6saDywKcG7Bi7w2FMAK2oTkuWfqhWRb5BEGmnSXdiCEPO5d-ytqP3nwlZXHaCDYscpP8bB4YLhvCn7R8Efw6gwQle24QPRP3lYoFeuUbDUq7GKA5SfaZUvWoeWjqyLIaBspKQsC26_Umx1E4IXLrMSL6nkRnrYcVZBAXrYCeTP1XtsV38_lZVJfHSaJaUy4PKaj3yvgm93EV2CXybPti7CCMXZ34VqqWiF64pQjZsPu3ZTr7ha_TTQq499-zYRQNDvIVsBDLQQIgrbctuGqj6lrXb31Jj3JIEYqH_4h5X9d0Q", "q": "1q-r-bmMFbIzrLK2U3elksZq8CqUqZxlSfkGMZuVkxgYMS-e4FPzEp2iirG-eO11aa0cpMMoBdTnVdGJ_ZUR93w0lGf9XnQAJqxP7eOsrUoiW4VWlWH4WfOiLgpO-pFtyTz_JksYYaotc_Z3Zy-Szw6a39IDbuYGy1qL-15oQuc", "p": "2lrYPppRbcQWu4LtWN6tOVUrtCOPv1eLTKTc7q8vCMcem1Ox5QFB7KnUtNZ5Ni7wnZUeVDfimNebtjNsGvDSrpgIlo9dEnFBQsQIkzZ2SkoYfgmF8hNdi6P-BfRjdgYouy4c6xAnGDgSMTip1YnPRyvbMaoYT9E_tEcBW5wOeoc", "kid": "a0", "kty": "RSA"}, {"use": "sig", "e": "AQAB", "d": "DodXDEtkovWWGsMEXYy_nEEMCWyROMOebCnCv0ey3i4M4bh2dmwqgz0e-IKQAFlGiMkidGL1lNbq0uFS04FbuRAR06dYw1cbrNbDdhrWFxKTd1L5D9p-x-gW-YDWhpI8rUGRa76JXkOSxZUbg09_QyUd99CXAHh-FXi_ZkIKD8hK6FrAs68qhLf8MNkUv63DTduw7QgeFfQivdopePxyGuMk5n8veqwsUZsklQkhNlTYQqeM1xb2698ZQcNYkl0OssEsSJKRjXt-LRPowKrdvTuTo2p--HMI0pIEeFs7H_u5OW3jihjvoFClGPynHQhgWmQzlQRvWRXh6FhDVqFeGQ", "n": "zfZzttF7HmnTYwSMPdxKs5AoczbNS2mOPz-tN1g4ljqI_F1DG8cgQDcN_VDufxoFGRERo2FK6WEN41LhbGEyP6uL6wW6Cy29qE9QZcvY5mXrncndRSOkNcMizvuEJes_fMYrmP_lPiC6kWiqItTk9QBWqJfiYKhCx9cSDXsBmJXn3KWQCVHvj1ANFWW0CWLMKlWN-_NMNLIWJN_pEAocTZMzxSFBK1b5_5J8ZS7hfWRF6MQmjsJcz2jzA21SQZNpre3kwnTGRSwo05sAS-TyeadDqQPWgbqX69UzcGq5irhzN8cpZ_JaTk3Y_uV6owanTZLVvCgdjaAnMYeZhb0KFw", "q": "5E5XKK5njT-zzRqqTeY2tgP9PJBACeaH_xQRHZ_1ydE7tVd7HdgdaEHfQ1jvKIHFkknWWOBAY1mlBc4YDirLShB_voShD8C-Hx3nF5sne5fleVfU-sZy6Za4B2U75PcE62oZgCPauOTAEm9Xuvrt5aMMovyzR8ecJZhm9bw7naU", "p": "5vJHCSM3H3q4RltYzENC9RyZZV8EUmpkv9moyguT5t-BUGA-T4W_FGIxzOPXRWOckIplKkoDKhavUeNmTZMCUcue0nkICSJpvNE4Nb2p5PZk_QqSdQNvCasQtdojEG0AmfVD85SU551CYxJdLdDFOqyK2entpMr8lhokem189As", "kid": "a1", "kty": "RSA"}, {"use": "sig", "crv": "P-256", "kty": "EC", "y": "Yg4IRzHBMIsuQK2Oz0Uukp1aNDnpdoyk6QBMtmfGHQQ", "x": "L0WUeVlc9r6YJd6ie9duvOU1RHwxSkJKA37IK9B4Bpc", "kid": "a2"}, {"use": "enc", "crv": "P-256", "kty": "EC", "y": "E6E6g5_ziIZvfdAoACctnwOhuQYMvQzA259aftPn59M", "x": "Yu8_BQE2L0f1MqnK0GumZOaj_77Tx70-LoudyRUnLM4", "kid": "a3"}]} \ No newline at end of file diff --git a/spec/lib/api/openid_connect/token_endpoint_spec.rb b/spec/lib/api/openid_connect/token_endpoint_spec.rb index 66554b81e..c03aa4102 100644 --- a/spec/lib/api/openid_connect/token_endpoint_spec.rb +++ b/spec/lib/api/openid_connect/token_endpoint_spec.rb @@ -7,6 +7,16 @@ describe Api::OpenidConnect::TokenEndpoint, type: :request do o_auth_application: client, user: bob, redirect_uri: "http://localhost:3000/", scopes: ["openid"]) } let!(:code) { auth.create_code } + let!(:client_with_specific_id) { FactoryGirl.create(:o_auth_application_with_ppid_with_specific_id) } + let!(:auth_with_specific_id) do + client_with_specific_id.client_id = "14d692cd53d9c1a9f46fd69e0e57443e" + client_with_specific_id.jwks_file = jwks_file_path + client_with_specific_id.save! + Api::OpenidConnect::Authorization.find_or_create_by( + o_auth_application: client_with_specific_id, + user: bob, redirect_uri: "http://localhost:3000/", scopes: ["openid"]) + end + let!(:code_with_specific_id) { auth_with_specific_id.create_code } describe "the authorization code grant type" do context "when the authorization code is valid" do @@ -53,6 +63,44 @@ describe Api::OpenidConnect::TokenEndpoint, type: :request do end end + context "when the authorization code is valid with jwt bearer" do + before do + post api_openid_connect_access_tokens_path, grant_type: "authorization_code", + redirect_uri: "http://localhost:3000/", code: code_with_specific_id, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImExIn0.eyJhdWQiOiBbImh0dHBzOi8va2VudHNoaWthbWEuY29tL2FwaS9vcGVuaWRfY29ubmVjdC9hY2Nlc3NfdG9rZW5zIl0sICJpc3MiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UiLCAianRpIjogIjBtY3JyZVlIIiwgImV4cCI6IDE0NDMxNzA4OTEuMzk3NDU2LCAiaWF0IjogMTQ0MzE3MDI5MS4zOTc0NTYsICJzdWIiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UifQ.QJUR3SYFrEIlbfOKjO0NYInddklytbJ2LSWNpkQ1aNThgneDCVCjIYGCaL2C9Sw-GR8j7QSUsKOwBbjZMUmVPFTjsfB4wdgObbxVt1QAXwDjAXc5w1smOerRsoahZ4yKI1an6PTaFxMwnoXUQcBZTsOS6RgXOCPPPoxibxohxoehPLieM0l7LYcF5DQKg7fTxZYOpmtiP--nibJxomXdVQNLSnZuQwnyWtlp_gYmqrYMMN1LPSmNCgZMZZZIYttaaAIA96SylglqubowJRShtDO9rSvUz_sgeCo7qo5Bfb0B5n9_PtIlr1CZSVoHyYj2lVqQldx7fnGuqqQJCfDQog" + end + + it "should return a valid id token" do + json = JSON.parse(response.body) + encoded_id_token = json["id_token"] + decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token, + Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY + expected_guid = bob.pairwise_pseudonymous_identifiers.find_by(identifier: "https://example.com/uri").guid + expect(decoded_token.sub).to eq(expected_guid) + expect(decoded_token.exp).to be > Time.zone.now.utc.to_i + end + + it "should return a valid access token" do + json = JSON.parse(response.body) + encoded_id_token = json["id_token"] + decoded_token = OpenIDConnect::ResponseObject::IdToken.decode encoded_id_token, + Api::OpenidConnect::IdTokenConfig::PUBLIC_KEY + access_token = json["access_token"] + access_token_check_num = UrlSafeBase64.encode64(OpenSSL::Digest::SHA256.digest(access_token)[0, 128 / 8]) + expect(decoded_token.at_hash).to eq(access_token_check_num) + end + + it "should not allow code to be reused" do + auth_with_specific_id.reload + expect(auth_with_specific_id.code).to eq(nil) + post api_openid_connect_access_tokens_path, grant_type: "authorization_code", + client_id: client.client_id, client_secret: client.client_secret, + redirect_uri: "http://localhost:3000/", code: code_with_specific_id + expect(JSON.parse(response.body)["error"]).to eq("invalid_grant") + end + end + context "when the authorization code is not valid" do it "should return an invalid grant error" do post api_openid_connect_access_tokens_path, grant_type: "authorization_code", @@ -61,6 +109,45 @@ describe Api::OpenidConnect::TokenEndpoint, type: :request do end end + context "when the client assertion is in an invalid format" do + before do + post api_openid_connect_access_tokens_path, grant_type: "authorization_code", + redirect_uri: "http://localhost:3000/", code: code_with_specific_id, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: "invalid_client_assertion.random" + end + + it "should return an error" do + expect(response.body).to include "invalid_request" + end + end + + context "when the client assertion is not matching with jwks keys" do + before do + post api_openid_connect_access_tokens_path, grant_type: "authorization_code", + redirect_uri: "http://localhost:3000/", code: code_with_specific_id, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImExIn0.eyJhdWQiOiBbImh0dHBzOi8va2VudHNoaWthbWEuY29tL2FwaS9vcGVuaWRfY29ubmVjdC9hY2Nlc3NfdG9rZW5zIl0sICJpc3MiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UiLCAianRpIjogIjBtY3JyZVlIIiwgImV4cCI6IDE0NDMxNzA4OTEuMzk3NDU2LCAiaWF0IjogMTQ0MzE3MDI5MS4zOTc0NTYsICJzdWIiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UifQ.QJUR3SYFrEIlbfOKjO0NYInddklytbJ2LSWNpkQ1aNThgneDCVCjIYGCaL2C9Sw-GR8j7QSUsKOwBbjZMUmVPFTjsfB4wdgObbxVt1QAXwDjAXc5w1smOerRsoahZ4yKI1an6PTaFxMwnoXUQcBZTsOS6RgXOCPPPoxibxohxoehPLieM0l7LYcF5DQKg7fTxZYOpmtiP--nibJxomXdVQNLSnZuQwnyWtlp_gYmqrYMMN1LPSmNCgZMZZZIYttaaAIA96SylglqubowJRShtDO9rSvUz_sgeCo7qo5Bfb0B5n9_PtIlr1CZSVoHyYj2lVqQldx7fnGuqqQJCfDQoe" + end + + it "should return an error" do + expect(response.body).to include "invalid_grant" + end + end + + context "when kid doesn't exist in jwks keys" do + before do + post api_openid_connect_access_tokens_path, grant_type: "authorization_code", + redirect_uri: "http://localhost:3000/", code: code_with_specific_id, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: "ewogIGFsZzogUlMyNTYsCiAga2lkOiBpbnZhbGlkX2tpZAp9Cg.eyJhdWQiOiBbImh0dHBzOi8va2VudHNoaWthbWEuY29tL2FwaS9vcGVuaWRfY29ubmVjdC9hY2Nlc3NfdG9rZW5zIl0sICJpc3MiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UiLCAianRpIjogIjBtY3JyZVlIIiwgImV4cCI6IDE0NDMxNzA4OTEuMzk3NDU2LCAiaWF0IjogMTQ0MzE3MDI5MS4zOTc0NTYsICJzdWIiOiAiMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2UifQ." + end + + it "should return an error" do + expect(response.body).to include "invalid_request" + end + end + context "when the client is unregistered" do it "should return an error" do post api_openid_connect_access_tokens_path, grant_type: "authorization_code", code: auth.refresh_token, @@ -69,6 +156,19 @@ describe Api::OpenidConnect::TokenEndpoint, type: :request do end end + context "when the client is unregistered with jwks keys" do + before do + post api_openid_connect_access_tokens_path, grant_type: "authorization_code", + redirect_uri: "http://localhost:3000/", code: code_with_specific_id, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + client_assertion: "eyJhbGciOiJSUzI1NiIsImtpZCI6ImExIn0.ewogIGF1ZDogWwogICAgaHR0cHM6Ly9rZW50c2hpa2FtYS5jb20vYXBpL29wZW5pZF9jb25uZWN0L2FjY2Vzc190b2tlbnMKICBdLAogIGlzczogMTRkNjkyY2Q1M2Q5YzFhOWY0NmZkNjllMGU1NzQ0M2QsCiAganRpOiAwbWNycmVZSCwKICBleHA6IDE0NDMxNzA4OTEuMzk3NDU2LAogIGlhdDogMTQ0MzE3MDI5MS4zOTc0NTYsCiAgc3ViOiAxNGQ2OTJjZDUzZDljMWE5ZjQ2ZmQ2OWUwZTU3NDQzZAp9Cg.QJUR3SYFrEIlbfOKjO0NYInddklytbJ2LSWNpkQ1aNThgneDCVCjIYGCaL2C9Sw-GR8j7QSUsKOwBbjZMUmVPFTjsfB4wdgObbxVt1QAXwDjAXc5w1smOerRsoahZ4yKI1an6PTaFxMwnoXUQcBZTsOS6RgXOCPPPoxibxohxoehPLieM0l7LYcF5DQKg7fTxZYOpmtiP--nibJxomXdVQNLSnZuQwnyWtlp_gYmqrYMMN1LPSmNCgZMZZZIYttaaAIA96SylglqubowJRShtDO9rSvUz_sgeCo7qo5Bfb0B5n9_PtIlr1CZSVoHyYj2lVqQldx7fnGuqqQJCfDQog" + end + + it "should return an error" do + expect(response.body).to include "invalid_request" + end + end + context "when the code field is missing" do it "should return an invalid request error" do post api_openid_connect_access_tokens_path, grant_type: "authorization_code", diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ca444256a..4fb30af00 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -59,6 +59,10 @@ def photo_fixture_name @photo_fixture_name = File.join(File.dirname(__FILE__), "fixtures", "button.png") end +def jwks_file_path + @jwks_file = "../../spec/fixtures/jwks.json" +end + # Force fixture rebuild FileUtils.rm_f(Rails.root.join("tmp", "fixture_builder.yml"))