├── Gemfile ├── Gemfile.lock ├── README.md ├── app ├── controllers │ └── redmine_oauth_controller.rb ├── helpers │ └── redmine_omniauth_helper.rb └── views │ ├── hooks │ └── _view_account_login_bottom.html.erb │ └── settings │ └── _google_settings.html.erb ├── assets ├── images │ └── google_login_icon.png └── stylesheets │ └── buttons.css ├── config ├── locales │ ├── en.yml │ └── ru.yml └── routes.rb ├── init.rb ├── lib ├── helpers │ ├── checker.rb │ └── mail_helper.rb └── redmine_omniauth_google │ └── hooks.rb └── test ├── functional └── redmine_oauth_controller_test.rb └── test_helper.rb /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'oauth2' 4 | gem 'json' 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | addressable (2.3.2) 5 | faraday (0.7.6) 6 | addressable (~> 2.2) 7 | multipart-post (~> 1.1) 8 | rack (~> 1.1) 9 | json (1.7.5) 10 | multi_json (1.3.6) 11 | multipart-post (1.1.5) 12 | oauth2 (0.5.2) 13 | faraday (~> 0.7) 14 | multi_json (~> 1.0) 15 | rack (1.4.1) 16 | 17 | PLATFORMS 18 | ruby 19 | 20 | DEPENDENCIES 21 | json 22 | oauth2 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Redmine omniauth google 2 | 3 | This plugin is used to authenticate Redmine users using Google's OAuth2 provider. 4 | 5 | ### Installation: 6 | 7 | Download the plugin and install required gems: 8 | 9 | ```console 10 | cd /path/to/redmine/plugins 11 | git clone https://github.com/twinslash/redmine_omniauth_google.git 12 | cd /path/to/redmine 13 | bundle install 14 | ``` 15 | 16 | Restart the app 17 | ```console 18 | touch /path/to/redmine/tmp/restart.txt 19 | ``` 20 | 21 | ### Registration 22 | 23 | To authenticate via Google you must first register your redmine instance via the Google Cloud Console 24 | 25 | * Go to the [registration](https://cloud.google.com/console) link. 26 | * Click your Project's name 27 | * Click "APIs & Auth" 28 | * Click "Registered Apps" 29 | * Click "Register App" 30 | * Type a name for the application, e.g. "My Redmine" 31 | * Select "Web Application" as the Platform 32 | * Click "Register" 33 | * Click "OAuth 2.0 Client ID" 34 | * Enter "https://mydomain.com/redmine/oauth2callback", where "mydomain.com/redmine" is the domain / path for your redmine instance. *** The plugin will not work without this setting *** 35 | * Click "Generate" 36 | * Save the Client ID and Client Secret for the configuration of the Redmine plugin (see below) 37 | 38 | ### Configuration 39 | 40 | * Login as a user with administrative privileges. 41 | * In top menu select "Administration". 42 | * Click "Plugins" 43 | * In plugins list, click "Configure" in the row for "Redmine Omniauth Google plugin" 44 | * Enter the Сlient ID & Client Secret shown when you registered your application via Google Cloud Console. 45 | * Check the box near "Oauth authentication" 46 | * Click Apply. 47 | 48 | Users can now to use their Google Account to log in to your instance of Redmine. 49 | 50 | Additionaly 51 | * Setup value Autologin in Settings on tab Authentification 52 | 53 | ### Other options 54 | 55 | By default, all user email domains are allowed to authenticate through Google. 56 | If you want to limit the user email domains allowed to use the plugin, list one per line in the "Allowed domains" text box. 57 | 58 | For example: 59 | 60 | ```text 61 | onedomain.com 62 | otherdomain.com 63 | ``` 64 | 65 | With the above configuration, only users with email addresses on the domains "onedomain.com" and "otherdomain.com" will be allowed to acccess your Redmine instance using Google OAuth. 66 | 67 | ### Authentication Workflow 68 | 69 | 1. An unauthenticated user requests the URL to your Redmine instance. 70 | 2. User clicks the "Login via Google" buton. 71 | 3. The plugin redirects them to a Google sign in page if they are not already signed in to their Google account. 72 | 4. Google redirects user back to Redmine, where the Google OAuth plugin's controller takes over. 73 | 74 | One of the following cases will occur: 75 | 1. If self-registration is enabled (Under Administration > Settings > Authentication), user is redirected to 'my/page' 76 | 2. Otherwse, the an account is created for the user (referencing their Google OAuth2 ID). A Redmine administrator must activate the account for it to work. 77 | -------------------------------------------------------------------------------- /app/controllers/redmine_oauth_controller.rb: -------------------------------------------------------------------------------- 1 | require 'account_controller' 2 | require 'json' 3 | 4 | class RedmineOauthController < AccountController 5 | include Helpers::MailHelper 6 | include Helpers::Checker 7 | def oauth_google 8 | if Setting.plugin_redmine_omniauth_google[:oauth_authentification] 9 | session[:back_url] = params[:back_url] 10 | redirect_to oauth_client.auth_code.authorize_url(:redirect_uri => oauth_google_callback_url, :scope => scopes) 11 | else 12 | password_authentication 13 | end 14 | end 15 | 16 | def oauth_google_callback 17 | if params[:error] 18 | flash[:error] = l(:notice_access_denied) 19 | redirect_to signin_path 20 | else 21 | token = oauth_client.auth_code.get_token(params[:code], :redirect_uri => oauth_google_callback_url) 22 | result = token.get('https://www.googleapis.com/oauth2/v1/userinfo') 23 | info = JSON.parse(result.body) 24 | if info && info["verified_email"] 25 | if allowed_domain_for?(info["email"]) 26 | try_to_login info 27 | else 28 | flash[:error] = l(:notice_domain_not_allowed, :domain => parse_email(info["email"])[:domain]) 29 | redirect_to signin_path 30 | end 31 | else 32 | flash[:error] = l(:notice_unable_to_obtain_google_credentials) 33 | redirect_to signin_path 34 | end 35 | end 36 | end 37 | 38 | def try_to_login info 39 | params[:back_url] = session[:back_url] 40 | session.delete(:back_url) 41 | user = User.joins(:email_addresses).where(:email_addresses => { :address => info["email"] }).first_or_create 42 | if user.new_record? 43 | # Self-registration off 44 | redirect_to(home_url) && return unless Setting.self_registration? 45 | # Create on the fly 46 | user.firstname, user.lastname = info["name"].split(' ') unless info['name'].nil? 47 | user.firstname ||= info[:given_name] 48 | user.lastname ||= info[:family_name] 49 | user.mail = info["email"] 50 | user.login = parse_email(info["email"])[:login] 51 | user.login ||= [user.firstname, user.lastname]*"." 52 | user.random_password 53 | user.register 54 | 55 | case Setting.self_registration 56 | when '1' 57 | register_by_email_activation(user) do 58 | onthefly_creation_failed(user) 59 | end 60 | when '3' 61 | register_automatically(user) do 62 | onthefly_creation_failed(user) 63 | end 64 | else 65 | register_manually_by_administrator(user) do 66 | onthefly_creation_failed(user) 67 | end 68 | end 69 | else 70 | # Existing record 71 | if user.active? 72 | successful_authentication(user) 73 | else 74 | # Redmine 2.4 adds an argument to account_pending 75 | if Redmine::VERSION::MAJOR > 2 or 76 | (Redmine::VERSION::MAJOR == 2 and Redmine::VERSION::MINOR >= 4) 77 | account_pending(user) 78 | else 79 | account_pending 80 | end 81 | end 82 | end 83 | end 84 | 85 | def oauth_client 86 | @client ||= OAuth2::Client.new(settings[:client_id], settings[:client_secret], 87 | :site => 'https://accounts.google.com', 88 | :authorize_url => '/o/oauth2/auth', 89 | :token_url => '/o/oauth2/token') 90 | end 91 | 92 | def settings 93 | @settings ||= Setting.plugin_redmine_omniauth_google 94 | end 95 | 96 | def scopes 97 | 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile' 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /app/helpers/redmine_omniauth_helper.rb: -------------------------------------------------------------------------------- 1 | module RedmineOmniauthHelper 2 | end 3 | -------------------------------------------------------------------------------- /app/views/hooks/_view_account_login_bottom.html.erb: -------------------------------------------------------------------------------- 1 | <%= stylesheet_link_tag 'buttons', :plugin => 'redmine_omniauth_google' %> 2 | 3 | <% if Setting.plugin_redmine_omniauth_google[:oauth_authentification] %> 4 | <%= link_to oauth_google_path(:back_url => back_url) do %> 5 | <%= button_tag :class => 'button-login' do %> 6 | <%= image_tag('/plugin_assets/redmine_omniauth_google/images/google_login_icon.png', :class => 'button-login-icon', :alt => l(:login_via_google)) %> 7 | <%= content_tag :div, l(:login_via_google), :class => 'button-login-text' %> 8 | <% end %> 9 | <% end %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /app/views/settings/_google_settings.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 3 | <%= text_field_tag 'settings[client_id]', @settings[:client_id] %> 4 |
5 |6 | 7 | <%= text_field_tag 'settings[client_secret]', @settings[:client_secret] %> 8 |
9 |10 | 11 | <%= text_area_tag "settings[allowed_domains]", @settings[:allowed_domains], :rows => 5 %> 12 |
13 |14 | 15 | <%= check_box_tag "settings[oauth_authentification]", true, @settings[:oauth_authentification] %> 16 |
17 | -------------------------------------------------------------------------------- /assets/images/google_login_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twinslash/redmine_omniauth_google/c4c79f5ab5599a6d963bb57131c9065bd2bf921e/assets/images/google_login_icon.png -------------------------------------------------------------------------------- /assets/stylesheets/buttons.css: -------------------------------------------------------------------------------- 1 | .button-login { 2 | position: relative; 3 | left: 45%; 4 | display: inline-block; 5 | border: 1px solid #999; 6 | border-radius: 2px; 7 | margin-top: 10px; 8 | width: 135px; 9 | height: 25px; 10 | padding: 0; 11 | } 12 | 13 | .button-login-icon { 14 | float: left; 15 | height: 18px; 16 | padding: 1px 0px 0px 4px; 17 | } 18 | 19 | .button-login-text { 20 | line-height: 21px; 21 | background-image: -webkit-linear-gradient(bottom, #ddd, white); 22 | font-size: 12px; 23 | } 24 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | notice_unable_to_obtain_google_credentials: "Unable to obtain credentials from Google." 3 | notice_domain_not_allowed: "You can not login using %{domain} domain." 4 | notice_access_denied: "You must allow to use you Google credentials to enter this site." 5 | login_via_google: "Login via Google" 6 | -------------------------------------------------------------------------------- /config/locales/ru.yml: -------------------------------------------------------------------------------- 1 | ru: 2 | notice_unable_to_obtain_google_credentials: "Не удалось получить данные от Google." 3 | notice_domain_not_allowed: "Вы не можете войти в систему при помощи домена %{domain}." 4 | notice_access_denied: "Для корректного входа необходимо разрешить приложению доступ к аккаунту." 5 | login_via_google: "Войти с Google" 6 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | get 'oauth_google', :to => 'redmine_oauth#oauth_google' 2 | get 'oauth2callback', :to => 'redmine_oauth#oauth_google_callback', :as => 'oauth_google_callback' 3 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'redmine' 2 | require_dependency 'redmine_omniauth_google/hooks' 3 | 4 | Redmine::Plugin.register :redmine_omniauth_google do 5 | name 'Redmine Omniauth Google plugin' 6 | author 'Dmitry Kovalenok' 7 | description 'This is a plugin for Redmine registration through google' 8 | version '0.0.1' 9 | url 'https://github.com/twinslash/redmine_omniauth_google' 10 | author_url 'http://twinslash.com' 11 | 12 | settings :default => { 13 | :client_id => "", 14 | :client_secret => "", 15 | :oauth_autentification => false, 16 | :allowed_domains => "" 17 | }, :partial => 'settings/google_settings' 18 | end 19 | -------------------------------------------------------------------------------- /lib/helpers/checker.rb: -------------------------------------------------------------------------------- 1 | module Helpers 2 | module Checker 3 | def allowed_domain_for? email 4 | allowed_domains = Setting.plugin_redmine_omniauth_google[:allowed_domains] 5 | return unless allowed_domains 6 | allowed_domains = allowed_domains.split 7 | return true if allowed_domains.empty? 8 | allowed_domains.index(parse_email(email)[:domain]) 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/helpers/mail_helper.rb: -------------------------------------------------------------------------------- 1 | module Helpers 2 | module MailHelper 3 | def parse_email email 4 | email_data = email && email.is_a?(String) ? email.match(/(.*?)@(.*)/) : nil 5 | {:login => email_data[1], :domain => email_data[2]} if email_data 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/redmine_omniauth_google/hooks.rb: -------------------------------------------------------------------------------- 1 | module RedmineOmniauthGoogle 2 | class Hooks < Redmine::Hook::ViewListener 3 | def view_account_login_bottom(context = {}) 4 | context[:controller].send(:render_to_string, { 5 | :partial => "hooks/view_account_login_bottom", 6 | :locals => context}) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/functional/redmine_oauth_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class RedmineOauthControllerTest < ActionController::TestCase 4 | include Helpers::MailHelper 5 | def setup 6 | @default_user_credentials = { :firstname => 'Cool', 7 | :lastname => 'User', 8 | :mail => 'user@somedomain.com'} 9 | @default_response_body = {:verified_email => true, 10 | :name => 'Cool User', 11 | :given_name => 'Cool', 12 | :family_name => 'User', 13 | :email => 'user@somedomain.com'} 14 | User.current = nil 15 | Setting.openid = '1' 16 | OAuth2::AccessToken.any_instance.stubs(:get => OAuth2::Response.new(nil)) 17 | OAuth2::Client.any_instance.stubs(:get_token => OAuth2::AccessToken.new('code', 'redirect_uri')) 18 | end 19 | 20 | #creates a new user with the credentials listed in the options and fills in the missing data by default data 21 | def new_user options = {} 22 | User.where(@default_user_credentials.merge(options)).delete_all 23 | user = User.new @default_user_credentials.merge(options) 24 | user.login = options[:login] || 'cool_user' 25 | user 26 | end 27 | 28 | #creates a new user with the credentials listed in the options and fills in the missing data by default data 29 | def set_response_body_stub options = {} 30 | OAuth2::Response.any_instance.stubs(:body => @default_response_body.merge(options).to_json) 31 | end 32 | 33 | def test_oauth_google_with_enabled_oauth_authentification 34 | Setting.plugin_redmine_omniauth_google[:oauth_authentification] = nil 35 | get :oauth_google 36 | assert_response 404 37 | end 38 | 39 | def test_oauth_google_callback_for_existing_non_active_user 40 | Setting.self_registration = '2' 41 | user = new_user :status => User::STATUS_REGISTERED 42 | assert user.save 43 | set_response_body_stub 44 | get :oauth_google_callback 45 | assert_redirected_to signin_path 46 | end 47 | 48 | def test_oauth_google_callback_for_existing_active_user 49 | user = new_user 50 | user.activate 51 | assert user.save 52 | set_response_body_stub 53 | get :oauth_google_callback 54 | assert_redirected_to :controller => 'my', :action => 'page' 55 | end 56 | 57 | def test_oauth_google_callback_for_new_user_with_valid_credentials_and_sefregistration_enabled 58 | Setting.self_registration = '3' 59 | set_response_body_stub 60 | get :oauth_google_callback 61 | assert_redirected_to :controller => 'my', :action => 'account' 62 | user = User.find_by_mail(@default_response_body[:email]) 63 | assert_equal user.mail, @default_response_body[:email] 64 | assert_equal user.login, parse_email(@default_response_body[:email])[:login] 65 | end 66 | 67 | def test_oauth_google_callback_for_new_user_with_valid_credentials_and_sefregistration_disabled 68 | Setting.self_registration = '2' 69 | set_response_body_stub 70 | get :oauth_google_callback 71 | assert_redirected_to signin_path 72 | end 73 | 74 | def test_oauth_google_callback_with_new_user_with_invalid_oauth_provider 75 | Setting.self_registration = '3' 76 | set_response_body_stub :verified_email => false 77 | get :oauth_google_callback 78 | assert_redirected_to signin_path 79 | end 80 | 81 | def test_oauth_google_callback_with_new_user_created_with_email_activation_should_have_a_token 82 | Setting.self_registration = '1' 83 | set_response_body_stub 84 | get :oauth_google_callback 85 | assert_redirected_to :signin 86 | user = User.find_by_mail(@default_user_credentials[:mail]) 87 | assert user 88 | token = Token.find_by_user_id_and_action(user.id, 'register') 89 | assert token 90 | end 91 | 92 | def test_oauth_google_callback_with_new_user_created_with_manual_activation 93 | Setting.self_registration = '2' 94 | set_response_body_stub 95 | get :oauth_google_callback 96 | assert_redirected_to :signin 97 | user = User.find_by_mail(@default_user_credentials[:mail]) 98 | assert user 99 | assert_equal User::STATUS_REGISTERED, user.status 100 | end 101 | 102 | def test_oauth_google_callback_with_not_allowed_email_domain 103 | Setting.plugin_redmine_omniauth_google[:allowed_domains] = "twinslash.com" 104 | set_response_body_stub 105 | get :oauth_google_callback 106 | assert_redirected_to :signin 107 | end 108 | 109 | def test_oauth_google_callback_with_allowed_email_domain 110 | Setting.self_registration = '3' 111 | Setting.plugin_redmine_omniauth_google[:allowed_domains] = parse_email(@default_response_body[:email])[:domain] 112 | set_response_body_stub 113 | get :oauth_google_callback 114 | assert_redirected_to :controller => 'my', :action => 'account' 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the Redmine helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | --------------------------------------------------------------------------------