Merge commit '14d85815cd04dd4250a7d453883ef6ee709c647f' as 'plugins/redmine_openid_connect'
This commit is contained in:
4
plugins/redmine_openid_connect/.gitignore
vendored
Normal file
4
plugins/redmine_openid_connect/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/.bundle
|
||||
/Gemfile.local
|
||||
.idea
|
||||
|
||||
21
plugins/redmine_openid_connect/CHANGELOG.md
Normal file
21
plugins/redmine_openid_connect/CHANGELOG.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## ??? 0.9.5
|
||||
* Pull server-side errors from locale files
|
||||
* Log-messages/some less prominent errors hard-coded in English again
|
||||
* Do not render `rpiframe`, if OpenID config does not contain a `check_session_iframe`
|
||||
* Some more documentation about setting up
|
||||
|
||||
## 0.9.4
|
||||
* Support Redmine 4
|
||||
* Upgrade deprecated calls
|
||||
|
||||
## 0.9.3
|
||||
* fix problem with symbols vs. strings usage
|
||||
|
||||
## 0.9.2
|
||||
* fix settings page
|
||||
* move to github
|
||||
* Avoid error if members_of is empty during check
|
||||
* Add disable ssl validation
|
||||
* Add protocol to hostname
|
||||
35
plugins/redmine_openid_connect/FusionAuth.md
Normal file
35
plugins/redmine_openid_connect/FusionAuth.md
Normal file
@@ -0,0 +1,35 @@
|
||||
## Connect to fusionAuth
|
||||
|
||||
## In FusionAuth
|
||||
|
||||
### Add App
|
||||
|
||||
* In Aplications >> Add a new app, choose a name and default options, you just need to pay attention to:
|
||||
|
||||
```
|
||||
Authorized redirect URLs add: http(or s)://your-host-url/oic/local_login http(or s)://your-host-url/oic/local_logout
|
||||
|
||||
Logout URL: http(or s)://your-host-url/oic/local_logout
|
||||
```
|
||||
|
||||
### Add role
|
||||
|
||||
In Application List, select the app and click manage roles, add two roles, User and Admin.
|
||||
|
||||
|
||||
|
||||
## In Redmine /settings/plugin/redmine_openid_connect
|
||||
|
||||
Settings>> Plugins >>Open id
|
||||
|
||||
* Just configure id and secret from Application Details in FusionAuth
|
||||
* OpenID Connect server url: http(or s)://your-fusion-url/
|
||||
* Scope: openid
|
||||
* Authorized group: User
|
||||
* Admins group: Admin
|
||||
|
||||
## Add the role to the user
|
||||
|
||||
* Just add the role to the users in FusionAuth and thats all,
|
||||
* User>>Manage>>Registrations add the app and the desidered role.
|
||||
* if you choose both roles, Redmine considerer the user as Admin
|
||||
2
plugins/redmine_openid_connect/Gemfile
Normal file
2
plugins/redmine_openid_connect/Gemfile
Normal file
@@ -0,0 +1,2 @@
|
||||
source 'https://rubygems.org'
|
||||
gem 'httparty', '~> 0.14.0'
|
||||
55
plugins/redmine_openid_connect/README.md
Normal file
55
plugins/redmine_openid_connect/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Redmine OpenID Connect Plugin #
|
||||
|
||||
Based on the work from [intelimina](https://bitbucket.org/intelimina/redmine_openid_connect) and [devopskube](https://github.com/devopskube).
|
||||
|
||||
## Introduction ##
|
||||
|
||||
This is a plugin based on the implementation of redmine_cas.
|
||||
|
||||
It redirects to an SSO server bypassing the original Redmine login authentication using the SSO server authentication in its place.
|
||||
|
||||
## Important ##
|
||||
|
||||
User registration is implicit and cannot be disabled at the moment.
|
||||
|
||||
So your OpenID provider should probably provide unique endpoints for your needs.
|
||||
|
||||
Check out [FusionAuth](https://fusionauth.io/) for an excellent solution.
|
||||
|
||||
## Server Settings ##
|
||||
|
||||
Just include `username` in the scope being sent and replied to the client app.
|
||||
|
||||
## Usage ##
|
||||
|
||||
### Configure Redmine ###
|
||||
|
||||
1. Go to your Redmine plugins directory.
|
||||
2. Clone/copy this plugin.
|
||||
3. Run `bundle install`
|
||||
4. Run `bundle exec rake redmine:plugins:migrate RAILS_ENV=production`
|
||||
5. Restart your server
|
||||
6. Login as administrator and head over to the plugins page.
|
||||
7. Open the configuration page for redmine openid connect plugin.
|
||||
8. Fill in the details.
|
||||
|
||||
### Configure Your OpenID Provider ###
|
||||
|
||||
1. Go to your SSO server and add these urls as authorized redirect urls:
|
||||
* `https://<your-redmine-domain>/oic/local_login`
|
||||
* `https://<your-redmine-domain>/oic/local_logout`
|
||||
2. Check the JWT Token generation. You need the following contents:
|
||||
* **`member_of`**: `String[]` of role/group names that your config maps to user properties like *is administrator* or *is authorized to log in*
|
||||
* **`user_name`**: `String` with the user's desired username (required for user creation), aliases: `nickname`, `preferred_username`
|
||||
* **`given_name`**: `String` with the user's first name (required for user creation)
|
||||
* **`family_name`**: `String` with the user's surname (required for user creation)
|
||||
* **`name`**: `String` with the user's full name (used as a fallback for first name and surname)
|
||||
* Should some of these fields be missing, try finding *Lambda* functions or *Generators* that allow you to customize the JWT Tokens issued
|
||||
|
||||
## In Case Your OpenID Provider Is Offline ##
|
||||
|
||||
If you enable the OpenId Connect plugin and your OpenId Connect Server is not reachable, but you still would like to login, you can use an additional parameter, to be able to login directly into redmine:
|
||||
|
||||
```https://<your-redmine-domain>/login?local_login=true```
|
||||
|
||||
Enjoy!
|
||||
264
plugins/redmine_openid_connect/app/models/oic_session.rb
Normal file
264
plugins/redmine_openid_connect/app/models/oic_session.rb
Normal file
@@ -0,0 +1,264 @@
|
||||
class OicSession < ActiveRecord::Base
|
||||
unloadable
|
||||
|
||||
before_create :randomize_state!
|
||||
before_create :randomize_nonce!
|
||||
|
||||
def self.client_config
|
||||
Setting.plugin_redmine_openid_connect
|
||||
end
|
||||
|
||||
def client_config
|
||||
self.class.client_config
|
||||
end
|
||||
|
||||
def self.host_name
|
||||
Setting.protocol + "://" + Setting.host_name
|
||||
end
|
||||
|
||||
def host_name
|
||||
self.class.host_name
|
||||
end
|
||||
|
||||
def self.enabled?
|
||||
client_config['enabled']
|
||||
end
|
||||
|
||||
def self.disabled?
|
||||
!self.enabled?
|
||||
end
|
||||
|
||||
def self.login_selector?
|
||||
client_config['login_selector']
|
||||
end
|
||||
|
||||
def self.create_user_if_not_exists?
|
||||
client_config['create_user_if_not_exists']
|
||||
end
|
||||
|
||||
def self.disallowed_auth_sources_login
|
||||
client_config['disallowed_auth_sources_login'].to_a
|
||||
end
|
||||
|
||||
def self.openid_configuration_url
|
||||
client_config['openid_connect_server_url'] + '/.well-known/openid-configuration'
|
||||
end
|
||||
|
||||
def self.get_dynamic_config
|
||||
hash = Digest::SHA1.hexdigest client_config.to_json
|
||||
expiry = client_config['dynamic_config_expiry'] || 86400
|
||||
Rails.cache.fetch("oic_session_dynamic_#{hash}", expires_in: expiry) do
|
||||
HTTParty::Basement.default_options.update(verify: false) if client_config['disable_ssl_validation']
|
||||
ActiveSupport::HashWithIndifferentAccess.new HTTParty.get(openid_configuration_url)
|
||||
end
|
||||
end
|
||||
|
||||
def self.dynamic_config
|
||||
@dynamic_config ||= get_dynamic_config
|
||||
end
|
||||
|
||||
def dynamic_config
|
||||
self.class.dynamic_config
|
||||
end
|
||||
|
||||
def self.get_token(query)
|
||||
uri = dynamic_config['token_endpoint']
|
||||
|
||||
HTTParty::Basement.default_options.update(verify: false) if client_config['disable_ssl_validation']
|
||||
response = HTTParty.post(
|
||||
uri,
|
||||
body: query,
|
||||
basic_auth: {username: client_config['client_id'], password: client_config['client_secret'] }
|
||||
)
|
||||
end
|
||||
|
||||
def get_access_token!
|
||||
response = self.class.get_token(access_token_query)
|
||||
if response["error"].blank?
|
||||
self.access_token = response["access_token"] if response["access_token"].present?
|
||||
self.refresh_token = response["refresh_token"] if response["refresh_token"].present?
|
||||
self.id_token = response["id_token"] if response["id_token"].present?
|
||||
self.expires_at = (DateTime.now + response["expires_in"].seconds) if response["expires_in"].present?
|
||||
self.save!
|
||||
end
|
||||
return response
|
||||
end
|
||||
|
||||
def refresh_access_token!
|
||||
response = self.class.get_token(refresh_token_query)
|
||||
if response["error"].blank?
|
||||
self.access_token = response["access_token"] if response["access_token"].present?
|
||||
self.refresh_token = response["refresh_token"] if response["refresh_token"].present?
|
||||
self.id_token = response["id_token"] if response["id_token"].present?
|
||||
self.expires_at = (DateTime.now + response["expires_in"].seconds) if response["expires_in"].present?
|
||||
self.save!
|
||||
end
|
||||
return response
|
||||
end
|
||||
|
||||
def self.parse_token(token)
|
||||
jwt = token.split('.')
|
||||
return JSON::parse(Base64::decode64(jwt[1]))
|
||||
end
|
||||
|
||||
def claims
|
||||
if @claims.blank? || id_token_changed?
|
||||
@claims = self.class.parse_token(id_token)
|
||||
end
|
||||
return @claims
|
||||
end
|
||||
|
||||
def get_user_info!
|
||||
uri = dynamic_config['userinfo_endpoint']
|
||||
|
||||
HTTParty::Basement.default_options.update(verify: false) if client_config['disable_ssl_validation']
|
||||
response = HTTParty.get(
|
||||
uri,
|
||||
headers: { "Authorization" => "Bearer #{access_token}" }
|
||||
)
|
||||
|
||||
if response.headers["content-type"] == 'application/jwt'
|
||||
# signed / encrypted response, extract before using
|
||||
return self.class.parse_token(response)
|
||||
else
|
||||
# unsigned response, just return the bare json
|
||||
return JSON::parse(response.body)
|
||||
decoded_token = response.body
|
||||
end
|
||||
end
|
||||
|
||||
def check_keycloak_role(role)
|
||||
# keycloak way...
|
||||
kc_is_in_role = false
|
||||
if user["realm_access"].present?
|
||||
kc_is_in_role = user["realm_access"]["roles"].include?(role)
|
||||
end
|
||||
if user["resource_access"].present? && user["resource_access"][client_config['client_id']].present?
|
||||
kc_is_in_role = user["resource_access"][client_config['client_id']]["roles"].include?(role)
|
||||
end
|
||||
return true if kc_is_in_role
|
||||
end
|
||||
|
||||
def authorized?
|
||||
if client_config['group'].blank?
|
||||
return true
|
||||
end
|
||||
|
||||
return true if check_keycloak_role client_config['group']
|
||||
|
||||
return false if !user["member_of"] && !user["roles"]
|
||||
|
||||
return true if self.admin?
|
||||
|
||||
if client_config['group'].present?
|
||||
return true if user["member_of"].present? && user["member_of"].include?(client_config['group'])
|
||||
return true if user["roles"].present? && user["roles"].include?(client_config['group']) || user["roles"].include?(client_config['admin_group'])
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
def admin?
|
||||
if client_config['admin_group'].present?
|
||||
if user["member_of"].present?
|
||||
return true if user["member_of"].include?(client_config['admin_group'])
|
||||
end
|
||||
if user["roles"].present?
|
||||
return true if user["roles"].include?(client_config['admin_group'])
|
||||
end
|
||||
# keycloak way...
|
||||
return true if check_keycloak_role client_config['admin_group']
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
def user
|
||||
if access_token? # keycloak way...
|
||||
@user = JSON::parse(Base64::decode64(access_token.split('.')[1]))
|
||||
else
|
||||
@user = JSON::parse(Base64::decode64(id_token.split('.')[1]))
|
||||
end
|
||||
return @user
|
||||
end
|
||||
|
||||
def authorization_url
|
||||
config = dynamic_config
|
||||
config["authorization_endpoint"] + "?" + authorization_query.to_param
|
||||
end
|
||||
|
||||
def end_session_url
|
||||
config = dynamic_config
|
||||
return if config["end_session_endpoint"].nil?
|
||||
config["end_session_endpoint"] + "?" + end_session_query.to_param
|
||||
end
|
||||
|
||||
def randomize_state!
|
||||
self.state = SecureRandom.uuid unless self.state.present?
|
||||
end
|
||||
|
||||
def randomize_nonce!
|
||||
self.nonce = SecureRandom.uuid unless self.nonce.present?
|
||||
end
|
||||
|
||||
def authorization_query
|
||||
query = {
|
||||
"response_type" => "code",
|
||||
"state" => self.state,
|
||||
"nonce" => self.nonce,
|
||||
"scope" => scopes,
|
||||
"redirect_uri" => "#{host_name}/oic/local_login",
|
||||
"client_id" => client_config['client_id'],
|
||||
}
|
||||
end
|
||||
|
||||
def access_token_query
|
||||
query = {
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => code,
|
||||
'scope' => scopes,
|
||||
'id_token' => id_token,
|
||||
'redirect_uri' => "#{host_name}/oic/local_login",
|
||||
}
|
||||
end
|
||||
|
||||
def refresh_token_query
|
||||
query = {
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => refresh_token,
|
||||
'scope' => scopes,
|
||||
}
|
||||
end
|
||||
|
||||
def end_session_query
|
||||
query = {
|
||||
'session_state' => session_state,
|
||||
'post_logout_redirect_uri' => "#{host_name}/oic/local_logout",
|
||||
}
|
||||
if id_token.present?
|
||||
query['id_token_hint'] = id_token
|
||||
end
|
||||
return query
|
||||
end
|
||||
|
||||
def expired?
|
||||
self.expires_at.nil? ? false : (self.expires_at < DateTime.now)
|
||||
end
|
||||
|
||||
def incomplete?
|
||||
self.access_token.blank?
|
||||
end
|
||||
|
||||
def complete?
|
||||
self.access_token.present?
|
||||
end
|
||||
|
||||
def scopes
|
||||
if client_config["scopes"].blank?
|
||||
return "openid profile email user_name"
|
||||
else
|
||||
client_config["scopes"].split(',').each(&:strip).join(' ')
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -0,0 +1,17 @@
|
||||
<%= javascript_tag do %>
|
||||
var wl = window.location
|
||||
, url
|
||||
, port;
|
||||
|
||||
if(wl.hash.length > 0) {
|
||||
if(wl.port == 80 || wl.port == 443) {
|
||||
port = null;
|
||||
} else {
|
||||
port = ':' + wl.port;
|
||||
}
|
||||
|
||||
url = wl.protocol + '//' + wl.hostname + port + wl.pathname + '?' + wl.hash.substring(1);
|
||||
wl.replace(url);
|
||||
document.write('<p class="oic-click-for-redirect">Click <a href="' + url + '">here</a>, if redirection is not working.</p>');
|
||||
}
|
||||
<% end %>
|
||||
@@ -0,0 +1,3 @@
|
||||
<p class="oic-logged-out">
|
||||
<%= l(:oic_logout_success, home_url).html_safe %>
|
||||
</p>
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>PMGov - RP iframe</title>
|
||||
<script type="text/javascript">
|
||||
var stat = "unchanged";
|
||||
var client_id = "<%= @oic_session.client_config['client_id'] %>";
|
||||
var check_interval = 5*1000;
|
||||
var source_origin = window.location.origin;
|
||||
var target_origin = new URL("<%= @oic_session.client_config['openid_connect_server_url'] %>").origin;
|
||||
var session_state = "<%= @oic_session.session_state %>";
|
||||
var mes = client_id + " " + session_state;
|
||||
var reauthorize_url = "<%= oic_local_logout_url %>";
|
||||
var timer = setInterval(checkSession(), check_interval);
|
||||
|
||||
function checkSession() {
|
||||
var opiframe = window.parent.document.getElementById("opiframe").contentWindow;
|
||||
opiframe.postMessage( mes, target_origin);
|
||||
}
|
||||
|
||||
window.addEventListener("message", receiveMessage, false);
|
||||
function receiveMessage(e) {
|
||||
if (e.origin !== target_origin) { return alert('Wrong target origin: ' + target_origin); }
|
||||
stat = e.data;
|
||||
|
||||
if (stat == "changed") {
|
||||
window.parent.location.href = reauthorize_url;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
<div id="login-form-openid">
|
||||
<%= form_tag(oic_login_url, :method => :get) do %>
|
||||
<%= back_url_hidden_field_tag %>
|
||||
|
||||
<input type="submit" name="login-openid" value="<%=l(:button_login_sso)%>" tabindex="5" id="login-submit-openid" />
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<% if oic_session.dynamic_config.key?('check_session_iframe') %>
|
||||
<iframe id="rpiframe" src="<%= oic_rpiframe_path %>" style="display:none" onload="checkSessionPoll('rpiframe')"></iframe>
|
||||
<iframe id="opiframe" src="<%= oic_session.dynamic_config['check_session_iframe'] %>" style="display:none" onload="checkSessionPoll('opiframe')"></iframe>
|
||||
<script type="text/javascript">
|
||||
checkSessionPoll = (function () {
|
||||
var rpiframe_loaded = false;
|
||||
var opiframe_loaded = false;
|
||||
return function(frame) {
|
||||
if (frame == 'rpiframe') rpiframe_loaded = true;
|
||||
if (frame == 'opiframe') opiframe_loaded = true;
|
||||
if (rpiframe_loaded && opiframe_loaded) {
|
||||
var rpiframe = document.getElementById('rpiframe').contentWindow;
|
||||
poll = setInterval(rpiframe.checkSession, rpiframe.check_interval);
|
||||
}
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<% else %>
|
||||
<script type="text/javascript">
|
||||
console.info('OpenID Connect did not specify a check_session_iframe URL.');
|
||||
</script>
|
||||
<% end %>
|
||||
@@ -0,0 +1,61 @@
|
||||
<h3><%= t('config.header') %></h3>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.enabled') %></label>
|
||||
<%= check_box_tag 'settings[enabled]', false, @settings['enabled'] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.client_id') %></label>
|
||||
<%= text_field_tag 'settings[client_id]', @settings['client_id'] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.openid_connect_server_url') %></label>
|
||||
<%= text_field_tag 'settings[openid_connect_server_url]', @settings['openid_connect_server_url'], :size => '60' %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.client_secret') %></label>
|
||||
<%= password_field_tag 'settings[client_secret]', @settings['client_secret'], :size => '60' %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.scopes') %></label>
|
||||
<%= text_field_tag 'settings[scopes]', @settings['scopes'], :size => '60' %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.group') %></label>
|
||||
<%= text_field_tag 'settings[group]', @settings['group'] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.admin_group') %></label>
|
||||
<%= text_field_tag 'settings[admin_group]', @settings['admin_group'] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.dynamic_config_expiry') %></label>
|
||||
<%= text_field_tag 'settings[dynamic_config_expiry]', @settings['dynamic_config_expiry'] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.disable_ssl_validation') %></label>
|
||||
<%= check_box_tag 'settings[disable_ssl_validation]', true, @settings['disable_ssl_validation'] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.login_selector') %></label>
|
||||
<%= check_box_tag 'settings[login_selector]', false, @settings['login_selector'] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.create_user_if_not_exists') %></label>
|
||||
<%= check_box_tag 'settings[create_user_if_not_exists]', true, @settings['create_user_if_not_exists'] %>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label><%= t('config.disallowed_auth_sources_login') %></label>
|
||||
<%= select_tag 'settings[disallowed_auth_sources_login]', options_for_select(AuthSource.all.map { |a| [a.name, a.id] }, OicSession.disallowed_auth_sources_login), :multiple => true, :include_blank => true, :size => 5 %>
|
||||
</p>
|
||||
@@ -0,0 +1,14 @@
|
||||
#login-form-openid {
|
||||
margin: 1em auto 2em auto;
|
||||
padding: 20px;
|
||||
width: 340px;
|
||||
border: 1px solid #FDBF3B;
|
||||
background-color: #FFEBC1;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#login-form-openid input[type=submit] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
19
plugins/redmine_openid_connect/config/locales/de.yml
Normal file
19
plugins/redmine_openid_connect/config/locales/de.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
de:
|
||||
config:
|
||||
enabled: Aktiv
|
||||
login_selector: Login-Auswahl
|
||||
header: OpenID Connect Konfiguration
|
||||
client_id: Client-ID
|
||||
openid_connect_server_url: OpenID Connect Server-Url
|
||||
scopes: OpenID Connect Scopes (kommasepariert)
|
||||
client_secret: Client-Secret
|
||||
group: Rolle "darf einloggen" (leer lassen, falls jeder authentifizierte User einloggen darf)
|
||||
admin_group: Rolle "Administratoren" (User mit dieser Rolle werden als Administrator behandelt)
|
||||
dynamic_config_expiry: "Intervall für Aktualisierung der OpenID-Einstellungen (Default: 1 day)"
|
||||
create_user_if_not_exists: "Benutzer erstellen, falls nicht vorhanden"
|
||||
disallowed_auth_sources_login: "Benutzer aus den folgenden Authentifizierungsquellen müssen sich mit SSO anmelden"
|
||||
oic_logout_success: 'Sie wurden ausgeloggt. <a href="%{value}">Klicken Sie hier, um sich erneut einzuloggen</a>.'
|
||||
oic_cannot_create_user: "Der Benutzer %{value} konnte nicht angelegt werden: "
|
||||
oic_try_another_account: "<a href='%{value}'>Mit einem anderen Account einloggen.</a>"
|
||||
oic_cannot_login_user: "Benutzer %{value} konnte sich nicht anmelden: Bitte melden Sie sich mit der SSO-Option an"
|
||||
button_login_sso: Melden Sie sich mit SSO an
|
||||
20
plugins/redmine_openid_connect/config/locales/en.yml
Normal file
20
plugins/redmine_openid_connect/config/locales/en.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
# English strings go here for Rails i18n
|
||||
en:
|
||||
config:
|
||||
enabled: Enabled
|
||||
login_selector: Login Selector
|
||||
header: OpenID Connect Configuration
|
||||
client_id: Client ID
|
||||
openid_connect_server_url: OpenID Connect server url
|
||||
scopes: OpenID Connect scopes (comma-separated)
|
||||
client_secret: Client Secret
|
||||
group: Authorized group (blank if all users are authorized)
|
||||
admin_group: Admins group (members of this group are treated as admin)
|
||||
dynamic_config_expiry: How often to retrieve openid configuration (default 1 day)
|
||||
create_user_if_not_exists: Create user if not exists
|
||||
disallowed_auth_sources_login: Users from the following auth sources will be required to login with SSO
|
||||
oic_logout_success: 'You have been logged out. <a href="%{value}">Click here to log in again</a>.'
|
||||
oic_cannot_create_user: "Could not create the user %{value}: "
|
||||
oic_try_another_account: "<a href='%{value}'>Try logging in with another account</a>"
|
||||
oic_cannot_login_user: "User %{value} could not login: Please login using the SSO option"
|
||||
button_login_sso: Login with SSO
|
||||
20
plugins/redmine_openid_connect/config/locales/pt.yml
Normal file
20
plugins/redmine_openid_connect/config/locales/pt.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
# Portuguese strings go here for Rails i18n
|
||||
pt:
|
||||
config:
|
||||
enabled: Ativado
|
||||
login_selector: Seletor de login
|
||||
header: "Configuração OpenID Connect"
|
||||
client_id: Client ID
|
||||
openid_connect_server_url: Url do servidor OpenID Connect
|
||||
scopes: OpenID Connect scopes (separados por virgula)
|
||||
client_secret: Client Secret
|
||||
group: "Grupo autorizado (vazio se todos os utilizadores são autorizados)"
|
||||
admin_group: "Grupo de Administradores (membros deste grupo são tratados como administradores)"
|
||||
dynamic_config_expiry: "Com que frequência obter configuração do openid (padrão 1 dia)"
|
||||
create_user_if_not_exists: "Criar utilizador caso não exista"
|
||||
disallowed_auth_sources_login: "Utilizadores das fontes selecionadas deverão fazer login SSO"
|
||||
oic_logout_success: 'Saiu com sucesso. <a href="%{value}">Clique aqui para voltar a entrar</a>.'
|
||||
oic_cannot_create_user: "Não foi possível criar o utilizador %{value}: "
|
||||
oic_try_another_account: "<a href='%{value}'>Tente entrar com uma conta diferente</a>"
|
||||
oic_cannot_login_user: "Não foi possível autenticar o utilizador %{value}: Por favor use o login SSO"
|
||||
button_login_sso: Entrar com SSO
|
||||
8
plugins/redmine_openid_connect/config/routes.rb
Normal file
8
plugins/redmine_openid_connect/config/routes.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# Plugin's routes
|
||||
# See: http://guides.rubyonrails.org/routing.html
|
||||
|
||||
get 'oic/login', to: 'account#oic_login'
|
||||
get 'oic/logout', to: 'account#oic_logout'
|
||||
get 'oic/local_login', to: 'account#oic_local_login'
|
||||
get 'oic/local_logout', to: 'account#oic_local_logout'
|
||||
get 'oic/rpiframe', to: 'account#rpiframe'
|
||||
2
plugins/redmine_openid_connect/contributors.txt
Normal file
2
plugins/redmine_openid_connect/contributors.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Alfonso Juan Dillera
|
||||
Markus M. May
|
||||
@@ -0,0 +1,25 @@
|
||||
class CreateOicSessions < ActiveRecord::Migration[4.2]
|
||||
def self.up
|
||||
create_table :oic_sessions do |t|
|
||||
t.references :user, foreign_key: { on_delete: :cascade }
|
||||
|
||||
t.text :code
|
||||
t.string :state
|
||||
t.string :nonce
|
||||
t.string :session_state
|
||||
t.text :id_token
|
||||
t.text :access_token
|
||||
t.text :refresh_token
|
||||
t.datetime :expires_at
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :oic_sessions, :user_id
|
||||
add_index :oic_sessions, :access_token, length: 64
|
||||
add_index :oic_sessions, :refresh_token, length: 64
|
||||
add_index :oic_sessions, :id_token, length: 64
|
||||
end
|
||||
def self.down
|
||||
drop_table :oic_sessions
|
||||
end
|
||||
end
|
||||
20
plugins/redmine_openid_connect/init.rb
Normal file
20
plugins/redmine_openid_connect/init.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
require 'redmine'
|
||||
require_relative 'lib/redmine_openid_connect/application_controller_patch'
|
||||
require_relative 'lib/redmine_openid_connect/account_controller_patch'
|
||||
require_relative 'lib/redmine_openid_connect/hooks'
|
||||
|
||||
Redmine::Plugin.register :redmine_openid_connect do
|
||||
name 'Redmine Openid Connect plugin'
|
||||
author 'Alfonso Juan Dillera / Markus M. May'
|
||||
description 'OpenID Connect implementation for Redmine'
|
||||
version '0.9.4'
|
||||
url 'https://github.com/devopskube/redmine_openid_connect'
|
||||
author_url 'http://github.com/adillera'
|
||||
|
||||
settings :default => { 'empty' => true }, partial: 'settings/redmine_openid_connect_settings'
|
||||
end
|
||||
|
||||
|
||||
ApplicationController.prepend(RedmineOpenidConnect::ApplicationControllerPatch)
|
||||
AccountController.prepend(RedmineOpenidConnect::AccountControllerPatch)
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
module RedmineOpenidConnect
|
||||
module AccountControllerPatch
|
||||
|
||||
def login
|
||||
if OicSession.disabled? || OicSession.login_selector? || params[:local_login].present? || request.post?
|
||||
return super
|
||||
end
|
||||
|
||||
redirect_to oic_login_url
|
||||
end
|
||||
|
||||
def logout
|
||||
if OicSession.disabled? || params[:local_login].present?
|
||||
return super
|
||||
end
|
||||
|
||||
oic_session = OicSession.find(session[:oic_session_id])
|
||||
oic_session.destroy
|
||||
logout_user
|
||||
reset_session
|
||||
redirect_to oic_session.end_session_url if oic_session.end_session_url
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
redirect_to oic_local_logout_url
|
||||
end
|
||||
|
||||
# performs redirect to SSO server
|
||||
def oic_login
|
||||
if session[:oic_session_id].blank?
|
||||
oic_session = OicSession.create
|
||||
session[:oic_session_id] = oic_session.id
|
||||
else
|
||||
begin
|
||||
oic_session = OicSession.find session[:oic_session_id]
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
oic_session = OicSession.create
|
||||
session[:oic_session_id] = oic_session.id
|
||||
end
|
||||
|
||||
if oic_session.complete? && oic_session.expired?
|
||||
response = oic_session.refresh_access_token!
|
||||
if response[:error].present?
|
||||
oic_session.destroy
|
||||
oic_session = OicSession.create
|
||||
session[:oic_session_id] = oic_session.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to oic_session.authorization_url
|
||||
end
|
||||
|
||||
def oic_local_logout
|
||||
logout_user
|
||||
reset_session
|
||||
end
|
||||
|
||||
def oic_local_login
|
||||
if params[:code]
|
||||
oic_session = OicSession.find(session[:oic_session_id])
|
||||
|
||||
unless oic_session.present?
|
||||
return invalid_credentials
|
||||
end
|
||||
|
||||
# verify request state or reauthorize
|
||||
unless oic_session.state == params[:state]
|
||||
flash[:error] = "Requête OpenID Connect invalide."
|
||||
return redirect_to oic_local_logout
|
||||
end
|
||||
|
||||
oic_session.update!(authorize_params)
|
||||
|
||||
# verify id token nonce or reauthorize
|
||||
if oic_session.id_token.present?
|
||||
unless oic_session.claims['nonce'] == oic_session.nonce
|
||||
flash[:error] = "ID Token invalide."
|
||||
return redirect_to oic_local_logout
|
||||
end
|
||||
end
|
||||
|
||||
# get access token and user info
|
||||
oic_session.get_access_token!
|
||||
user_info = oic_session.get_user_info!
|
||||
|
||||
# verify application authorization
|
||||
unless oic_session.authorized?
|
||||
return invalid_credentials
|
||||
end
|
||||
|
||||
# Check if there's already an existing user
|
||||
user = User.find_by_mail(user_info["email"])
|
||||
|
||||
if user.nil?
|
||||
if !OicSession.create_user_if_not_exists?
|
||||
flash.now[:warning] ||= l(:oic_cannot_create_user, value: user_info["email"])
|
||||
|
||||
logger.warn "Could not create user #{user_info["email"]}, the system is not allowed to create new users through openid"
|
||||
flash.now[:warning] += "The system is not allowed to create new users through openid"
|
||||
|
||||
return invalid_credentials
|
||||
end
|
||||
|
||||
user = User.new
|
||||
|
||||
user.login = user_info["user_name"] || user_info["nickname"] || user_info["preferred_username"] || user_info["email"]
|
||||
|
||||
firstname = user_info["given_name"]
|
||||
lastname = user_info["family_name"]
|
||||
|
||||
if (firstname.nil? || lastname.nil?) && user_info["name"]
|
||||
parts = user_info["name"].split
|
||||
if parts.length >= 2
|
||||
firstname = parts[0]
|
||||
lastname = parts[-1]
|
||||
end
|
||||
end
|
||||
|
||||
attributes = {
|
||||
firstname: firstname || "",
|
||||
lastname: lastname || "",
|
||||
mail: user_info["email"],
|
||||
mail_notification: 'only_my_events',
|
||||
last_login_on: Time.now
|
||||
}
|
||||
|
||||
user.assign_attributes attributes
|
||||
|
||||
if user.save
|
||||
user.update_attribute(:admin, oic_session.admin?)
|
||||
oic_session.user_id = user.id
|
||||
oic_session.save!
|
||||
# after user creation just show "My Page" don't redirect to remember
|
||||
successful_authentication(user)
|
||||
else
|
||||
flash.now[:warning] ||= l(:oic_cannot_create_user, value:user.login)
|
||||
user.errors.full_messages.each do |error|
|
||||
logger.warn "Could not create user #{user.login}, error was #{error}"
|
||||
flash.now[:warning] += "#{error}. "
|
||||
end
|
||||
return invalid_credentials
|
||||
end
|
||||
else
|
||||
user.update_attribute(:admin, oic_session.admin?)
|
||||
oic_session.user_id = user.id
|
||||
oic_session.save!
|
||||
# redirect back to initial URL
|
||||
if session[:remember_url]
|
||||
params[:back_url] = session[:remember_url]
|
||||
session[:remember_url] = nil
|
||||
end
|
||||
successful_authentication(user)
|
||||
end # if user.nil?
|
||||
end
|
||||
end
|
||||
|
||||
def password_authentication
|
||||
user = User.find_by_login(params[:username])
|
||||
if OicSession.enabled? and !user.nil? and !user.auth_source.nil? and OicSession.disallowed_auth_sources_login.map(&:to_i).include? user.auth_source.id
|
||||
flash.now[:warning] ||= l(:oic_cannot_login_user, params[:username])
|
||||
logger.warn "User #{params[:username]} cannot login because it was disallowed by the openid plugin configuration"
|
||||
else
|
||||
return super
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_credentials
|
||||
return super unless OicSession.enabled?
|
||||
|
||||
logger.warn "Failed login attempt for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}"
|
||||
flash.now[:error] = (l(:notice_account_invalid_credentials) + " " + l(:oic_try_another_account, signout_path)).html_safe
|
||||
end
|
||||
|
||||
def rpiframe
|
||||
@oic_session = OicSession.find(session[:oic_session_id])
|
||||
render layout: false
|
||||
end
|
||||
|
||||
def authorize_params
|
||||
# compatible with both rails 3 and 4
|
||||
if params.respond_to?(:permit)
|
||||
params.permit(
|
||||
:code,
|
||||
:id_token,
|
||||
:session_state,
|
||||
)
|
||||
else
|
||||
params.select do |k,v|
|
||||
[
|
||||
'code',
|
||||
'id_token',
|
||||
'session_state',
|
||||
].include?(k)
|
||||
end
|
||||
end
|
||||
end
|
||||
end # AccountControllerPatch
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
module RedmineOpenidConnect
|
||||
module ApplicationControllerPatch
|
||||
def require_login
|
||||
return super unless (OicSession.enabled? && !OicSession.login_selector?)
|
||||
|
||||
if !User.current.logged?
|
||||
if request.get?
|
||||
url = request.original_url
|
||||
else
|
||||
url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
|
||||
end
|
||||
session[:remember_url] = url
|
||||
redirect_to oic_login_url
|
||||
return false
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# set the current user _without_ resetting the session first
|
||||
def logged_user=(user)
|
||||
return super(user) unless OicSession.enabled?
|
||||
|
||||
if user && user.is_a?(User)
|
||||
User.current = user
|
||||
start_user_session(user)
|
||||
else
|
||||
User.current = User.anonymous
|
||||
end
|
||||
end
|
||||
end # ApplicationControllerPatch
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
require_relative '../../app/models/oic_session'
|
||||
|
||||
module RedmineOpenidConnect
|
||||
class Hooks < Redmine::Hook::ViewListener
|
||||
def request
|
||||
ActionDispatch::Request.new(ENV)
|
||||
end
|
||||
def session
|
||||
request.session
|
||||
end
|
||||
|
||||
def view_account_login_bottom(context={})
|
||||
return unless (OicSession.enabled? && OicSession.login_selector?)
|
||||
if context[:request].session[:oic_session_id].blank?
|
||||
oic_session = OicSession.create
|
||||
else
|
||||
oic_session = OicSession.find context[:request].session[:oic_session_id]
|
||||
end
|
||||
context[:oic_session] = oic_session
|
||||
context[:controller].send(:render_to_string, {
|
||||
partial: 'hooks/redmine_openid_connect/view_account_login_bottom',
|
||||
locals: context
|
||||
})
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
end
|
||||
|
||||
def view_layouts_base_body_bottom(context={})
|
||||
return unless OicSession.enabled?
|
||||
oic_session = OicSession.find context[:request].session[:oic_session_id]
|
||||
context[:oic_session] = oic_session
|
||||
context[:controller].send(:render_to_string, {
|
||||
partial: 'hooks/redmine_openid_connect/view_layouts_base_body_bottom',
|
||||
locals: context
|
||||
})
|
||||
rescue ActiveRecord::RecordNotFound => e
|
||||
end
|
||||
|
||||
def view_layouts_base_html_head(context={})
|
||||
stylesheet_link_tag(:redmine_openid_connect, :plugin => 'redmine_openid_connect')
|
||||
end
|
||||
end
|
||||
end
|
||||
2
plugins/redmine_openid_connect/test/test_helper.rb
Normal file
2
plugins/redmine_openid_connect/test/test_helper.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
# Load the Redmine helper
|
||||
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
|
||||
@@ -0,0 +1,9 @@
|
||||
require File.expand_path('../../test_helper', __FILE__)
|
||||
|
||||
class OicSessionTest < ActiveSupport::TestCase
|
||||
|
||||
# Replace this with your real tests.
|
||||
def test_truth
|
||||
assert true
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user