Commit d27f461f authored by Drew Blessing's avatar Drew Blessing Committed by Vitali Tatarintev

Add OmniAuth sign-in via Atlassian Cloud

Adds a new OmniAuth integration for signing in via Atlassian
Cloud. The stored identity will also include credentials so
GitLab can communicate with the Atlassian API.
parent 4e24daa6
...@@ -45,6 +45,7 @@ gem 'omniauth_crowd', '~> 2.4.0' ...@@ -45,6 +45,7 @@ gem 'omniauth_crowd', '~> 2.4.0'
gem 'omniauth-authentiq', '~> 0.3.3' gem 'omniauth-authentiq', '~> 0.3.3'
gem 'omniauth_openid_connect', '~> 0.3.5' gem 'omniauth_openid_connect', '~> 0.3.5'
gem 'omniauth-salesforce', '~> 1.0.5' gem 'omniauth-salesforce', '~> 1.0.5'
gem 'omniauth-atlassian-oauth2', '~> 0.2.0'
gem 'rack-oauth2', '~> 1.9.3' gem 'rack-oauth2', '~> 1.9.3'
gem 'jwt', '~> 2.1.0' gem 'jwt', '~> 2.1.0'
......
...@@ -737,6 +737,9 @@ GEM ...@@ -737,6 +737,9 @@ GEM
omniauth (1.9.0) omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0) hashie (>= 3.4.6, < 3.7.0)
rack (>= 1.6.2, < 3) rack (>= 1.6.2, < 3)
omniauth-atlassian-oauth2 (0.2.0)
omniauth (>= 1.1.1)
omniauth-oauth2 (>= 1.5)
omniauth-auth0 (2.0.0) omniauth-auth0 (2.0.0)
omniauth-oauth2 (~> 1.4) omniauth-oauth2 (~> 1.4)
omniauth-authentiq (0.3.3) omniauth-authentiq (0.3.3)
...@@ -1393,6 +1396,7 @@ DEPENDENCIES ...@@ -1393,6 +1396,7 @@ DEPENDENCIES
octokit (~> 4.15) octokit (~> 4.15)
oj (~> 3.10.6) oj (~> 3.10.6)
omniauth (~> 1.8) omniauth (~> 1.8)
omniauth-atlassian-oauth2 (~> 0.2.0)
omniauth-auth0 (~> 2.0.0) omniauth-auth0 (~> 2.0.0)
omniauth-authentiq (~> 0.3.3) omniauth-authentiq (~> 0.3.3)
omniauth-azure-oauth2 (~> 0.0.9) omniauth-azure-oauth2 (~> 0.0.9)
......
...@@ -83,6 +83,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -83,6 +83,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end end
end end
def atlassian_oauth2
omniauth_flow(Gitlab::Auth::Atlassian)
end
private private
def log_failed_login(user, provider) def log_failed_login(user, provider)
......
...@@ -7,10 +7,9 @@ class Profiles::AccountsController < Profiles::ApplicationController ...@@ -7,10 +7,9 @@ class Profiles::AccountsController < Profiles::ApplicationController
render(locals: show_view_variables) render(locals: show_view_variables)
end end
# rubocop: disable CodeReuse/ActiveRecord
def unlink def unlink
provider = params[:provider] provider = params[:provider]
identity = current_user.identities.find_by(provider: provider) identity = find_identity(provider)
return render_404 unless identity return render_404 unless identity
...@@ -22,13 +21,18 @@ class Profiles::AccountsController < Profiles::ApplicationController ...@@ -22,13 +21,18 @@ class Profiles::AccountsController < Profiles::ApplicationController
redirect_to profile_account_path redirect_to profile_account_path
end end
# rubocop: enable CodeReuse/ActiveRecord
private private
def show_view_variables def show_view_variables
{} {}
end end
def find_identity(provider)
return current_user.atlassian_identity if provider == 'atlassian_oauth2'
current_user.identities.find_by(provider: provider) # rubocop: disable CodeReuse/ActiveRecord
end
end end
Profiles::AccountsController.prepend_if_ee('EE::Profiles::AccountsController') Profiles::AccountsController.prepend_if_ee('EE::Profiles::AccountsController')
# frozen_string_literal: true # frozen_string_literal: true
module AuthHelper module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce).freeze PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq salesforce atlassian_oauth2).freeze
LDAP_PROVIDER = /\Aldap/.freeze LDAP_PROVIDER = /\Aldap/.freeze
def ldap_enabled? def ldap_enabled?
...@@ -133,6 +133,8 @@ module AuthHelper ...@@ -133,6 +133,8 @@ module AuthHelper
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def auth_active?(provider) def auth_active?(provider)
return current_user.atlassian_identity.present? if provider == :atlassian_oauth2
current_user.identities.exists?(provider: provider.to_s) current_user.identities.exists?(provider: provider.to_s)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
---
title: Add OmniAuth sign-in via Atlassian Cloud
merge_request: 40178
author:
type: added
...@@ -1404,6 +1404,11 @@ test: ...@@ -1404,6 +1404,11 @@ test:
app_id: 'YOUR_CLIENT_ID', app_id: 'YOUR_CLIENT_ID',
app_secret: 'YOUR_CLIENT_SECRET' app_secret: 'YOUR_CLIENT_SECRET'
} }
- { name: 'atlassian_oauth2',
app_id: 'YOUR_CLIENT_ID',
app_secret: 'YOUR_CLIENT_SECRET',
args: { scope: 'offline_access read:jira-user read:jira-work', prompt: 'consent' }
}
ldap: ldap:
enabled: false enabled: false
servers: servers:
......
...@@ -11,6 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -11,6 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
GitLab integrates with the following external authentication and authorization GitLab integrates with the following external authentication and authorization
providers: providers:
- [Atlassian](atlassian.md)
- [Auth0](../../integration/auth0.md) - [Auth0](../../integration/auth0.md)
- [Authentiq](authentiq.md) - [Authentiq](authentiq.md)
- [AWS Cognito](cognito.md) - [AWS Cognito](cognito.md)
......
---
type: reference
stage: Manage
group: Access
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Atlassian OmniAuth Provider
To enable the Atlassian OmniAuth provider for passwordless authentication you must register an application with Atlassian.
## Atlassian application registration
1. Go to <https://developer.atlassian.com/apps/> and sign-in with the Atlassian
account that will administer the application.
1. Click **Create a new app**.
1. Choose an App Name, such as 'GitLab', and click **Create**.
1. Note the `Client ID` and `Secret` for the [GitLab configuration](#gitlab-configuration) steps.
1. In the left sidebar under **APIS AND FEATURES**, click **OAuth 2.0 (3LO)**.
1. Enter the GitLab callback URL using the format `https://gitlab.example.com/users/auth/atlassian_oauth2/callback` and click **Save changes**.
1. Click **+ Add** in the left sidebar under **APIS AND FEATURES**.
1. Click **Add** for **Jira platform REST API** and then **Configure**.
1. Click **Add** next to the following scopes:
- **View Jira issue data**
- **View user profiles**
- **Create and manage issues**
## GitLab configuration
1. On your GitLab server, open the configuration file:
For Omnibus GitLab installations:
```shell
sudo editor /etc/gitlab/gitlab.rb
```
For installations from source:
```shell
sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
```
1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add `atlassian_oauth2` as an OAuth provider.
1. Add the provider configuration for Atlassian:
For Omnibus GitLab installations:
```ruby
gitlab_rails['omniauth_providers'] = [
{
name: "atlassian_oauth2",
app_id: "YOUR_CLIENT_ID",
app_secret: "YOUR_CLIENT_SECRET",
args: { scope: 'offline_access read:jira-user read:jira-work', prompt: 'consent' }
}
]
```
For installations from source:
```yaml
- name: "atlassian_oauth2",
app_id: "YOUR_CLIENT_ID",
app_secret: "YOUR_CLIENT_SECRET",
args: { scope: 'offline_access read:jira-user read:jira-work', prompt: 'consent' }
```
1. Change `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` to the Client credentials you received in [application registration](#atlassian-application-registration) steps.
1. Save the configuration file.
1. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source) for the changes to take effect if you installed GitLab via Omnibus or from source respectively.
On the sign-in page there should now be an Atlassian icon below the regular sign in form. Click the icon to begin the authentication process.
If everything goes right, the user is signed in to GitLab using their Atlassian credentials.
# frozen_string_literal: true
module Gitlab
module Auth
module Atlassian
class AuthHash < Gitlab::Auth::OAuth::AuthHash
def token
credentials[:token]
end
def refresh_token
credentials[:refresh_token]
end
def expires?
credentials[:expires]
end
def expires_at
credentials[:expires_at]
end
private
def credentials
auth_hash[:credentials]
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Auth
module Atlassian
class IdentityLinker < OmniauthIdentityLinkerBase
extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize
private
override :identity
def identity
strong_memoize(:identity) do
current_user.atlassian_identity || build_atlassian_identity
end
end
def build_atlassian_identity
identity = current_user.build_atlassian_identity
::Gitlab::Auth::Atlassian::User.assign_identity_from_auth_hash!(identity, auth_hash)
end
def auth_hash
::Gitlab::Auth::Atlassian::AuthHash.new(oauth)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Auth
module Atlassian
class User < Gitlab::Auth::OAuth::User
def self.assign_identity_from_auth_hash!(identity, auth_hash)
identity.extern_uid = auth_hash.uid
identity.token = auth_hash.token
identity.refresh_token = auth_hash.refresh_token
identity.expires_at = Time.at(auth_hash.expires_at).utc.to_datetime if auth_hash.expires?
identity
end
protected
def find_by_uid_and_provider
::Atlassian::Identity.find_by_extern_uid(auth_hash.uid)&.user
end
def add_or_update_user_identities
return unless gl_user
identity = gl_user.atlassian_identity || gl_user.build_atlassian_identity
self.class.assign_identity_from_auth_hash!(identity, auth_hash)
end
def auth_hash=(auth_hash)
@auth_hash = ::Gitlab::Auth::Atlassian::AuthHash.new(auth_hash)
end
end
end
end
end
...@@ -5,10 +5,11 @@ module Gitlab ...@@ -5,10 +5,11 @@ module Gitlab
module OAuth module OAuth
class Provider class Provider
LABELS = { LABELS = {
"github" => "GitHub", "github" => "GitHub",
"gitlab" => "GitLab.com", "gitlab" => "GitLab.com",
"google_oauth2" => "Google", "google_oauth2" => "Google",
"azure_oauth2" => "Azure AD" "azure_oauth2" => "Azure AD",
'atlassian_oauth2' => 'Atlassian'
}.freeze }.freeze
def self.authentication(user, provider) def self.authentication(user, provider)
......
...@@ -276,6 +276,51 @@ RSpec.describe OmniauthCallbacksController, type: :controller do ...@@ -276,6 +276,51 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
end end
end end
context 'atlassian_oauth2' do
let(:provider) { :atlassian_oauth2 }
let(:extern_uid) { 'my-uid' }
context 'when the user and identity already exist' do
let(:user) { create(:atlassian_user, extern_uid: extern_uid) }
it 'allows sign-in' do
post :atlassian_oauth2
expect(request.env['warden']).to be_authenticated
end
end
context 'for a new user' do
before do
stub_omniauth_setting(enabled: true, auto_link_user: true, allow_single_sign_on: ['atlassian_oauth2'])
user.destroy
end
it 'denies sign-in if sign-up is enabled, but block_auto_created_users is set' do
post :atlassian_oauth2
expect(flash[:alert]).to start_with 'Your account has been blocked.'
end
it 'accepts sign-in if sign-up is enabled' do
stub_omniauth_setting(block_auto_created_users: false)
post :atlassian_oauth2
expect(request.env['warden']).to be_authenticated
end
it 'denies sign-in if sign-up is not enabled' do
stub_omniauth_setting(allow_single_sign_on: false, block_auto_created_users: false)
post :atlassian_oauth2
expect(flash[:alert]).to start_with 'Signing in using your Atlassian account without a pre-existing GitLab account is not allowed.'
end
end
end
context 'salesforce' do context 'salesforce' do
let(:extern_uid) { 'my-uid' } let(:extern_uid) { 'my-uid' }
let(:provider) { :salesforce } let(:provider) { :salesforce }
......
...@@ -45,5 +45,18 @@ RSpec.describe Profiles::AccountsController do ...@@ -45,5 +45,18 @@ RSpec.describe Profiles::AccountsController do
end end
end end
end end
describe 'atlassian_oauth2 provider' do
let(:user) { create(:atlassian_user) }
it 'allows a user to unlink a connected account' do
expect(user.atlassian_identity).not_to be_nil
delete :unlink, params: { provider: 'atlassian_oauth2' }
expect(response).to have_gitlab_http_status(:found)
expect(user.reload.atlassian_identity).to be_nil
end
end
end end
end end
...@@ -136,6 +136,16 @@ FactoryBot.define do ...@@ -136,6 +136,16 @@ FactoryBot.define do
end end
end end
factory :atlassian_user do
transient do
extern_uid { generate(:username) }
end
after(:create) do |user, evaluator|
create(:atlassian_identity, user: user, extern_uid: evaluator.extern_uid)
end
end
factory :admin, traits: [:admin] factory :admin, traits: [:admin]
end end
end end
...@@ -220,4 +220,44 @@ RSpec.describe AuthHelper do ...@@ -220,4 +220,44 @@ RSpec.describe AuthHelper do
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
end end
describe '#auth_active?' do
let(:user) { create(:user) }
def auth_active?
helper.auth_active?(provider)
end
before do
allow(helper).to receive(:current_user).and_return(user)
end
context 'for atlassian_oauth2 provider' do
let_it_be(:provider) { :atlassian_oauth2 }
it 'returns true when present' do
create(:atlassian_identity, user: user)
expect(auth_active?).to be true
end
it 'returns false when not present' do
expect(auth_active?).to be false
end
end
context 'for other omniauth providers' do
let_it_be(:provider) { 'google_oauth2' }
it 'returns true when present' do
create(:identity, provider: provider, user: user)
expect(auth_active?).to be true
end
it 'returns false when not present' do
expect(auth_active?).to be false
end
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Auth::Atlassian::AuthHash do
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(uid: 'john', credentials: credentials)
)
end
let(:credentials) do
{
token: 'super_secret_token',
refresh_token: 'super_secret_refresh_token',
expires_at: 2.weeks.from_now.to_i,
expires: true
}
end
describe '#uid' do
it 'returns the correct uid' do
expect(auth_hash.uid).to eq('john')
end
end
describe '#token' do
it 'returns the correct token' do
expect(auth_hash.token).to eq(credentials[:token])
end
end
describe '#refresh_token' do
it 'returns the correct refresh token' do
expect(auth_hash.refresh_token).to eq(credentials[:refresh_token])
end
end
describe '#token' do
it 'returns the correct expires boolean' do
expect(auth_hash.expires?).to eq(credentials[:expires])
end
end
describe '#token' do
it 'returns the correct expiration' do
expect(auth_hash.expires_at).to eq(credentials[:expires_at])
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Auth::Atlassian::IdentityLinker do
let(:user) { create(:user) }
let(:extern_uid) { generate(:username) }
let(:oauth) do
OmniAuth::AuthHash.new(
uid: extern_uid,
provider: 'atlassian_oauth2',
info: { name: 'John', email: 'john@mail.com' },
credentials: credentials
)
end
let(:credentials) do
{
token: SecureRandom.alphanumeric(1254),
refresh_token: SecureRandom.alphanumeric(45),
expires_at: 2.weeks.from_now.to_i,
expires: true
}
end
subject { described_class.new(user, oauth) }
context 'linked identity exists' do
let!(:identity) { create(:atlassian_identity, user: user, extern_uid: extern_uid) }
before do
subject.link
end
it 'sets #changed? to false' do
expect(subject).not_to be_changed
end
it 'does not mark as failed' do
expect(subject).not_to be_failed
end
end
context 'identity already linked to different user' do
let!(:identity) { create(:atlassian_identity, extern_uid: extern_uid) }
it 'sets #changed? to false' do
subject.link
expect(subject).not_to be_changed
end
it 'exposes error message' do
expect(subject.error_message).to eq 'Extern uid has already been taken'
end
end
context 'identity needs to be created' do
let(:identity) { user.atlassian_identity }
before do
subject.link
end
it_behaves_like 'an atlassian identity'
it 'sets #changed? to true' do
expect(subject).to be_changed
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Auth::Atlassian::User do
let(:oauth_user) { described_class.new(oauth) }
let(:gl_user) { oauth_user.gl_user }
let(:extern_uid) { generate(:username) }
let(:oauth) do
OmniAuth::AuthHash.new(
uid: extern_uid,
provider: 'atlassian_oauth2',
info: { name: 'John', email: 'john@mail.com' },
credentials: credentials)
end
let(:credentials) do
{
token: SecureRandom.alphanumeric(1254),
refresh_token: SecureRandom.alphanumeric(45),
expires_at: 2.weeks.from_now.to_i,
expires: true
}
end
describe '.assign_identity_from_auth_hash!' do
let(:auth_hash) { ::Gitlab::Auth::Atlassian::AuthHash.new(oauth) }
let(:identity) { described_class.assign_identity_from_auth_hash!(Atlassian::Identity.new, auth_hash) }
it_behaves_like 'an atlassian identity'
end
describe '#save' do
context 'for an existing user' do
context 'with an existing Atlassian Identity' do
let!(:existing_user) { create(:atlassian_user, extern_uid: extern_uid) }
let(:identity) { gl_user.atlassian_identity }
before do
oauth_user.save # rubocop:disable Rails/SaveBang
end
it 'finds the existing user and identity' do
expect(gl_user.id).to eq(existing_user.id)
expect(identity.id).to eq(existing_user.atlassian_identity.id)
end
it_behaves_like 'an atlassian identity'
end
context 'for a new user' do
it 'creates the user and identity' do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
end
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'an atlassian identity' do
it 'sets the proper values' do
expect(identity.extern_uid).to eq(extern_uid)
expect(identity.token).to eq(credentials[:token])
expect(identity.refresh_token).to eq(credentials[:refresh_token])
expect(identity.expires_at.to_i).to eq(credentials[:expires_at])
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment