From c4982890489d254da2fe998aab30bf257767ed5e Mon Sep 17 00:00:00 2001 From: Markus Koller Date: Fri, 9 Dec 2016 18:36:50 +0100 Subject: Implement OpenID Connect identity provider --- Gemfile | 1 + Gemfile.lock | 13 ++ app/models/oauth_access_grant.rb | 4 + app/models/oauth_access_token.rb | 2 +- app/views/doorkeeper/authorizations/new.html.haml | 2 + changelogs/unreleased/feature-openid-connect.yml | 4 + config/initializers/doorkeeper.rb | 7 +- config/initializers/doorkeeper_openid_connect.rb | 36 ++++++ config/initializers/secret_token.rb | 7 +- config/locales/doorkeeper.en.yml | 1 + config/routes.rb | 2 + ...5216_create_doorkeeper_openid_connect_tables.rb | 37 ++++++ ...lidate_foreign_keys_on_oauth_openid_requests.rb | 20 +++ db/schema.rb | 6 + doc/integration/README.md | 1 + doc/integration/openid_connect_provider.md | 47 ++++++++ lib/gitlab/auth.rb | 2 +- spec/factories/oauth_access_grants.rb | 11 ++ spec/factories/oauth_access_tokens.rb | 3 +- spec/factories/oauth_applications.rb | 2 +- spec/initializers/secret_token_spec.rb | 25 +++- spec/requests/openid_connect_spec.rb | 134 +++++++++++++++++++++ spec/routing/openid_connect_spec.rb | 30 +++++ 23 files changed, 388 insertions(+), 9 deletions(-) create mode 100644 app/models/oauth_access_grant.rb create mode 100644 changelogs/unreleased/feature-openid-connect.yml create mode 100644 config/initializers/doorkeeper_openid_connect.rb create mode 100644 db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb create mode 100644 db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb create mode 100644 doc/integration/openid_connect_provider.md create mode 100644 spec/factories/oauth_access_grants.rb create mode 100644 spec/requests/openid_connect_spec.rb create mode 100644 spec/routing/openid_connect_spec.rb diff --git a/Gemfile b/Gemfile index 4ac5a0ccfc1..b66cd38b4dc 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,7 @@ gem 'rugged', '~> 0.24.0' # Authentication libraries gem 'devise', '~> 4.2' gem 'doorkeeper', '~> 4.2.0' +gem 'doorkeeper-openid_connect', '~> 1.1.0' gem 'omniauth', '~> 1.4.2' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' diff --git a/Gemfile.lock b/Gemfile.lock index d4131a3dede..62388628eaa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,7 @@ GEM better_errors (1.0.1) coderay (>= 1.0.0) erubis (>= 2.6.6) + bindata (2.3.5) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) bootstrap-sass (3.3.6) @@ -167,6 +168,9 @@ GEM unf (>= 0.0.5, < 1.0.0) doorkeeper (4.2.0) railties (>= 4.2) + doorkeeper-openid_connect (1.1.2) + doorkeeper (~> 4.0) + json-jwt (~> 1.6) dropzonejs-rails (0.7.2) rails (> 3.1) email_reply_trimmer (0.1.6) @@ -376,6 +380,12 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.6) + json-jwt (1.7.1) + activesupport + bindata + multi_json (>= 1.3) + securecompare + url_safe_base64 json-schema (2.6.2) addressable (~> 2.3.8) jwt (1.5.6) @@ -684,6 +694,7 @@ GEM scss_lint (0.47.1) rake (>= 0.9, < 11) sass (~> 3.4.15) + securecompare (1.0.0) seed-fu (2.3.6) activerecord (>= 3.1) activesupport (>= 3.1) @@ -789,6 +800,7 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) + url_safe_base64 (0.2.2) validates_hostname (1.0.6) activerecord (>= 3.0) activesupport (>= 3.0) @@ -866,6 +878,7 @@ DEPENDENCIES devise-two-factor (~> 3.0.0) diffy (~> 3.1.0) doorkeeper (~> 4.2.0) + doorkeeper-openid_connect (~> 1.1.0) dropzonejs-rails (~> 0.7.1) email_reply_trimmer (~> 0.1) email_spec (~> 1.6.0) diff --git a/app/models/oauth_access_grant.rb b/app/models/oauth_access_grant.rb new file mode 100644 index 00000000000..3a997406565 --- /dev/null +++ b/app/models/oauth_access_grant.rb @@ -0,0 +1,4 @@ +class OauthAccessGrant < Doorkeeper::AccessGrant + belongs_to :resource_owner, class_name: 'User' + belongs_to :application, class_name: 'Doorkeeper::Application' +end diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb index 116fb71ac08..b85f5dbaf2e 100644 --- a/app/models/oauth_access_token.rb +++ b/app/models/oauth_access_token.rb @@ -1,4 +1,4 @@ -class OauthAccessToken < ActiveRecord::Base +class OauthAccessToken < Doorkeeper::AccessToken belongs_to :resource_owner, class_name: 'User' belongs_to :application, class_name: 'Doorkeeper::Application' end diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index a196561f381..82aa51f9778 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -27,6 +27,7 @@ = hidden_field_tag :state, @pre_auth.state = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope + = hidden_field_tag :nonce, @pre_auth.nonce = submit_tag "Authorize", class: "btn btn-success wide pull-left" = form_tag oauth_authorization_path, method: :delete do = hidden_field_tag :client_id, @pre_auth.client.uid @@ -34,4 +35,5 @@ = hidden_field_tag :state, @pre_auth.state = hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :scope, @pre_auth.scope + = hidden_field_tag :nonce, @pre_auth.nonce = submit_tag "Deny", class: "btn btn-danger prepend-left-10" diff --git a/changelogs/unreleased/feature-openid-connect.yml b/changelogs/unreleased/feature-openid-connect.yml new file mode 100644 index 00000000000..e84eb7aff86 --- /dev/null +++ b/changelogs/unreleased/feature-openid-connect.yml @@ -0,0 +1,4 @@ +--- +title: Implement OpenID Connect identity provider +merge_request: 8018 +author: Markus Koller diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 88cd0f5f652..52551a5d5eb 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -8,7 +8,12 @@ Doorkeeper.configure do # Put your resource owner authentication logic here. # Ensure user is redirected to redirect_uri after login session[:user_return_to] = request.fullpath - current_user || redirect_to(new_user_session_url) + if current_user + current_user + else + redirect_to(new_user_session_url) + nil + end end resource_owner_from_credentials do |routes| diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb new file mode 100644 index 00000000000..700ca25b884 --- /dev/null +++ b/config/initializers/doorkeeper_openid_connect.rb @@ -0,0 +1,36 @@ +Doorkeeper::OpenidConnect.configure do + issuer Gitlab.config.gitlab.url + + jws_private_key Rails.application.secrets.jws_private_key + + resource_owner_from_access_token do |access_token| + User.active.find_by(id: access_token.resource_owner_id) + end + + auth_time_from_resource_owner do |user| + user.current_sign_in_at + end + + reauthenticate_resource_owner do |user, return_to| + store_location_for user, return_to + sign_out user + redirect_to new_user_session_url + end + + subject do |user| + # hash the user's ID with the Rails secret_key_base to avoid revealing it + Digest::SHA256.hexdigest "#{user.id}-#{Rails.application.secrets.secret_key_base}" + end + + claims do + with_options scope: :openid do |o| + o.claim(:name) { |user| user.name } + o.claim(:nickname) { |user| user.username } + o.claim(:email) { |user| user.public_email } + o.claim(:email_verified) { |user| true if user.public_email? } + o.claim(:website) { |user| user.full_website_url if user.website_url? } + o.claim(:profile) { |user| Rails.application.routes.url_helpers.user_url user } + o.claim(:picture) { |user| user.avatar_url } + end + end +end diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 291fa6c0abc..f9c1d2165d3 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -24,7 +24,8 @@ def create_tokens defaults = { secret_key_base: file_secret_key || generate_new_secure_token, otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token, - db_key_base: generate_new_secure_token + db_key_base: generate_new_secure_token, + jws_private_key: generate_new_rsa_private_key } missing_secrets = set_missing_keys(defaults) @@ -41,6 +42,10 @@ def generate_new_secure_token SecureRandom.hex(64) end +def generate_new_rsa_private_key + OpenSSL::PKey::RSA.new(2048).to_pem +end + def warn_missing_secret(secret) warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml." end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 1d728282d90..14d49885fb3 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -60,6 +60,7 @@ en: scopes: api: Access your API read_user: Read user information + openid: Authenticate using OpenID Connect flash: applications: diff --git a/config/routes.rb b/config/routes.rb index 06293316937..1a851da6203 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,8 @@ Rails.application.routes.draw do authorizations: 'oauth/authorizations' end + use_doorkeeper_openid_connect + # Autocomplete get '/autocomplete/users' => 'autocomplete#users' get '/autocomplete/users/:id' => 'autocomplete#user' diff --git a/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb new file mode 100644 index 00000000000..e63d5927f86 --- /dev/null +++ b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb @@ -0,0 +1,37 @@ +class CreateDoorkeeperOpenidConnectTables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + create_table :oauth_openid_requests do |t| + t.integer :access_grant_id, null: false + t.string :nonce, null: false + end + + if Gitlab::Database.postgresql? + # add foreign key without validation to avoid downtime on PostgreSQL, + # also see db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb + execute %q{ + ALTER TABLE "oauth_openid_requests" + ADD CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id" + FOREIGN KEY ("access_grant_id") + REFERENCES "oauth_access_grants" ("id") + NOT VALID; + } + else + execute %q{ + ALTER TABLE oauth_openid_requests + ADD CONSTRAINT fk_oauth_openid_requests_oauth_access_grants_access_grant_id + FOREIGN KEY (access_grant_id) + REFERENCES oauth_access_grants (id); + } + end + end + + def down + drop_table :oauth_openid_requests + end +end diff --git a/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb new file mode 100644 index 00000000000..e206f9af636 --- /dev/null +++ b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb @@ -0,0 +1,20 @@ +class ValidateForeignKeysOnOauthOpenidRequests < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + if Gitlab::Database.postgresql? + execute %q{ + ALTER TABLE "oauth_openid_requests" + VALIDATE CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"; + } + end + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index 624cf9432d0..8984ab3aac1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -878,6 +878,11 @@ ActiveRecord::Schema.define(version: 20170306170512) do add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + create_table "oauth_openid_requests", force: :cascade do |t| + t.integer "access_grant_id", null: false + t.string "nonce", null: false + end + create_table "pages_domains", force: :cascade do |t| t.integer "project_id" t.text "certificate" @@ -1374,6 +1379,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade + add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id" add_foreign_key "personal_access_tokens", "users" add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade diff --git a/doc/integration/README.md b/doc/integration/README.md index 22bdf33443d..e56e58498a6 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -12,6 +12,7 @@ See the documentation below for details on how to configure these services. - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](cas.md) Configure GitLab to sign in using CAS - [OAuth2 provider](oauth_provider.md) OAuth2 application creation +- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages - [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users - [Akismet](akismet.md) Configure Akismet to stop spam diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md new file mode 100644 index 00000000000..56f367d841e --- /dev/null +++ b/doc/integration/openid_connect_provider.md @@ -0,0 +1,47 @@ +# GitLab as OpenID Connect identity provider + +This document is about using GitLab as an OpenID Connect identity provider +to sign in to other services. + +## Introduction to OpenID Connect + +[OpenID Connect] \(OIC) is a simple identity layer on top of the +OAuth 2.0 protocol. It allows clients to verify the identity of the end-user +based on the authentication performed by GitLab, as well as to obtain +basic profile information about the end-user in an interoperable and +REST-like manner. OIC performs many of the same tasks as OpenID 2.0, +but does so in a way that is API-friendly, and usable by native and +mobile applications. + +On the client side, you can use [omniauth-openid-connect] for Rails +applications, or any of the other available [client implementations]. + +GitLab's implementation uses the [doorkeeper-openid_connect] gem, refer +to its README for more details about which parts of the specifications +are supported. + +## Enabling OpenID Connect for OAuth applications + +Refer to the [OAuth guide] for basic information on how to set up OAuth +applications in GitLab. To enable OIC for an application, all you have to do +is select the `openid` scope in the application settings. + +Currently the following user information is shared with clients: + +| Claim | Type | Description | +|:-----------------|:----------|:------------| +| `sub` | `string` | An opaque token that uniquely identifies the user +| `auth_time` | `integer` | The timestamp for the user's last authentication +| `name` | `string` | The user's full name +| `nickname` | `string` | The user's GitLab username +| `email` | `string` | The user's public email address +| `email_verified` | `boolean` | Whether the user's public email address was verified +| `website` | `string` | URL for the user's website +| `profile` | `string` | URL for the user's GitLab profile +| `picture` | `string` | URL for the user's GitLab avatar + +[OpenID Connect]: http://openid.net/connect/ "OpenID Connect website" +[doorkeeper-openid_connect]: https://github.com/doorkeeper-gem/doorkeeper-openid_connect "Doorkeeper::OpenidConnect website" +[OAuth guide]: oauth_provider.md "GitLab as OAuth2 authentication service provider" +[omniauth-openid-connect]: https://github.com/jjbohn/omniauth-openid-connect/ "OmniAuth::OpenIDConnect website" +[client implementations]: http://openid.net/developers/libraries#connect "List of available client implementations" diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 0a0bd0e781c..6166f8d0dd9 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -2,7 +2,7 @@ module Gitlab module Auth MissingPersonalTokenError = Class.new(StandardError) - SCOPES = [:api, :read_user].freeze + SCOPES = [:api, :read_user, :openid, :profile, :email].freeze DEFAULT_SCOPES = [:api].freeze OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES diff --git a/spec/factories/oauth_access_grants.rb b/spec/factories/oauth_access_grants.rb new file mode 100644 index 00000000000..543b3e99274 --- /dev/null +++ b/spec/factories/oauth_access_grants.rb @@ -0,0 +1,11 @@ +FactoryGirl.define do + factory :oauth_access_grant do + resource_owner_id { create(:user).id } + application + token { Doorkeeper::OAuth::Helpers::UniqueToken.generate } + expires_in 2.hours + + redirect_uri { application.redirect_uri } + scopes { application.scopes } + end +end diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb index ccf02d0719b..a46bc1d8ce8 100644 --- a/spec/factories/oauth_access_tokens.rb +++ b/spec/factories/oauth_access_tokens.rb @@ -2,6 +2,7 @@ FactoryGirl.define do factory :oauth_access_token do resource_owner application - token '123456' + token { Doorkeeper::OAuth::Helpers::UniqueToken.generate } + scopes { application.scopes } end end diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb index d116a573830..86cdc208268 100644 --- a/spec/factories/oauth_applications.rb +++ b/spec/factories/oauth_applications.rb @@ -1,7 +1,7 @@ FactoryGirl.define do factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do name { FFaker::Name.name } - uid { FFaker::Name.name } + uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate } redirect_uri { FFaker::Internet.uri('http') } owner owner_type 'User' diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index ad7f032d1e5..65c97da2efd 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -6,6 +6,9 @@ describe 'create_tokens', lib: true do let(:secrets) { ActiveSupport::OrderedOptions.new } + HEX_KEY = /\h{128}/ + RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m + before do allow(File).to receive(:write) allow(File).to receive(:delete) @@ -15,7 +18,7 @@ describe 'create_tokens', lib: true do allow(self).to receive(:exit) end - context 'setting secret_key_base and otp_key_base' do + context 'setting secret keys' do context 'when none of the secrets exist' do before do stub_env('SECRET_KEY_BASE', nil) @@ -24,19 +27,29 @@ describe 'create_tokens', lib: true do allow(self).to receive(:warn_missing_secret) end - it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do + it 'generates different hashes for secret_key_base, otp_key_base, and db_key_base' do create_tokens keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base) expect(keys.uniq).to eq(keys) - expect(keys.map(&:length)).to all(eq(128)) + expect(keys).to all(match(HEX_KEY)) + end + + it 'generates an RSA key for jws_private_key' do + create_tokens + + keys = secrets.values_at(:jws_private_key) + + expect(keys.uniq).to eq(keys) + expect(keys).to all(match(RSA_KEY)) end it 'warns about the secrets to add to secrets.yml' do expect(self).to receive(:warn_missing_secret).with('secret_key_base') expect(self).to receive(:warn_missing_secret).with('otp_key_base') expect(self).to receive(:warn_missing_secret).with('db_key_base') + expect(self).to receive(:warn_missing_secret).with('jws_private_key') create_tokens end @@ -48,6 +61,7 @@ describe 'create_tokens', lib: true do expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base) expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base) expect(new_secrets['db_key_base']).to eq(secrets.db_key_base) + expect(new_secrets['jws_private_key']).to eq(secrets.jws_private_key) end create_tokens @@ -63,6 +77,7 @@ describe 'create_tokens', lib: true do context 'when the other secrets all exist' do before do secrets.db_key_base = 'db_key_base' + secrets.jws_private_key = 'jws_private_key' allow(File).to receive(:exist?).with('.secret').and_return(true) allow(File).to receive(:read).with('.secret').and_return('file_key') @@ -73,6 +88,7 @@ describe 'create_tokens', lib: true do stub_env('SECRET_KEY_BASE', 'env_key') secrets.secret_key_base = 'secret_key_base' secrets.otp_key_base = 'otp_key_base' + secrets.jws_private_key = 'jws_private_key' end it 'does not issue a warning' do @@ -98,6 +114,7 @@ describe 'create_tokens', lib: true do before do secrets.secret_key_base = 'secret_key_base' secrets.otp_key_base = 'otp_key_base' + secrets.jws_private_key = 'jws_private_key' end it 'does not write any files' do @@ -112,6 +129,7 @@ describe 'create_tokens', lib: true do expect(secrets.secret_key_base).to eq('secret_key_base') expect(secrets.otp_key_base).to eq('otp_key_base') expect(secrets.db_key_base).to eq('db_key_base') + expect(secrets.jws_private_key).to eq('jws_private_key') end it 'deletes the .secret file' do @@ -135,6 +153,7 @@ describe 'create_tokens', lib: true do expect(new_secrets['secret_key_base']).to eq('file_key') expect(new_secrets['otp_key_base']).to eq('file_key') expect(new_secrets['db_key_base']).to eq('db_key_base') + expect(new_secrets['jws_private_key']).to eq('jws_private_key') end create_tokens diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb new file mode 100644 index 00000000000..5206634bca5 --- /dev/null +++ b/spec/requests/openid_connect_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe 'OpenID Connect requests' do + include ApiHelpers + + let(:user) { create :user } + let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id } + let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id } + + def request_access_token + login_as user + + post '/oauth/token', + grant_type: 'authorization_code', + code: access_grant.token, + redirect_uri: application.redirect_uri, + client_id: application.uid, + client_secret: application.secret + end + + def request_user_info + get '/oauth/userinfo', nil, 'Authorization' => "Bearer #{access_token.token}" + end + + def hashed_subject + Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}") + end + + context 'Application without OpenID scope' do + let(:application) { create :oauth_application, scopes: 'api' } + + it 'token response does not include an ID token' do + request_access_token + + expect(json_response).to include 'access_token' + expect(json_response).not_to include 'id_token' + end + + it 'userinfo response is unauthorized' do + request_user_info + + expect(response).to have_http_status 403 + expect(response.body).to be_blank + end + end + + context 'Application with OpenID scope' do + let(:application) { create :oauth_application, scopes: 'openid' } + + it 'token response includes an ID token' do + request_access_token + + expect(json_response).to include 'id_token' + end + + context 'UserInfo payload' do + let(:user) do + create( + :user, + name: 'Alice', + username: 'alice', + emails: [private_email, public_email], + email: private_email.email, + public_email: public_email.email, + website_url: 'https://example.com', + avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png"), + ) + end + + let(:public_email) { build :email, email: 'public@example.com' } + let(:private_email) { build :email, email: 'private@example.com' } + + it 'includes all user information' do + request_user_info + + expect(json_response).to eq({ + 'sub' => hashed_subject, + 'name' => 'Alice', + 'nickname' => 'alice', + 'email' => 'public@example.com', + 'email_verified' => true, + 'website' => 'https://example.com', + 'profile' => 'http://localhost/alice', + 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png", + }) + end + end + + context 'ID token payload' do + before do + request_access_token + @payload = JSON::JWT.decode(json_response['id_token'], :skip_verification) + end + + it 'includes the Gitlab root URL' do + expect(@payload['iss']).to eq Gitlab.config.gitlab.url + end + + it 'includes the hashed user ID' do + expect(@payload['sub']).to eq hashed_subject + end + + it 'includes the time of the last authentication' do + expect(@payload['auth_time']).to eq user.current_sign_in_at.to_i + end + + it 'does not include any unknown properties' do + expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time] + end + end + + context 'when user is blocked' do + it 'returns authentication error' do + access_grant + user.block + + expect do + request_access_token + end.to throw_symbol :warden + end + end + + context 'when user is ldap_blocked' do + it 'returns authentication error' do + access_grant + user.ldap_block + + expect do + request_access_token + end.to throw_symbol :warden + end + end + end +end diff --git a/spec/routing/openid_connect_spec.rb b/spec/routing/openid_connect_spec.rb new file mode 100644 index 00000000000..2c3bc08f1a1 --- /dev/null +++ b/spec/routing/openid_connect_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys +# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider +# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger +describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do + it "to #provider" do + expect(get('/.well-known/openid-configuration')).to route_to('doorkeeper/openid_connect/discovery#provider') + end + + it "to #webfinger" do + expect(get('/.well-known/webfinger')).to route_to('doorkeeper/openid_connect/discovery#webfinger') + end + + it "to #keys" do + expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys') + end +end + +# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show +# POST /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show +describe Doorkeeper::OpenidConnect::UserinfoController, 'routing' do + it "to #show" do + expect(get('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show') + end + + it "to #show" do + expect(post('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show') + end +end -- cgit v1.2.3