Alternativecustom authentication HowTo » History » Revision 10
« Previous |
Revision 10/11
(diff)
| Next »
Andrew R Jackson, 2014-04-08 21:01
Updated auth_sources table info to reflect schema as of Redmine 2.4.1
Alternative (custom) Authentication HowTo¶
- Table of contents
- Alternative (custom) Authentication HowTo
Intro¶
This page explains how to get Redmine to authenticate users against a different database. Perhaps you're running (and even wrote) an app that already stores account records, and you want Redmine to use those. Presumably that app doesn't support OpenID, else you'd be configuring that.
Having Redmine defer authentication to your other app is helpful to users--they only have to remember one set of passwords and the accounts can't get out-of-sync. And if they are registered with your main app, Redmine can be configured to automatically add them to its own table (without storing any password info) when they first log in there.
Redmine Support For Alternative Authentication¶
Redmine has specific support for alternative/custom authentication which makes implementing it very easy.auth_sources
table- You will add a record here specific to your custom authentication.
AuthSource
class- You will create your own subclass of this, and implement the
authenticate()
method.
- You will create your own subclass of this, and implement the
- First, try to authenticate
login
&password
against Redmine's internal table (users
). - If that fails, try each alternative authentication source registered in the
auth_sources
table, stopping when one of the sources succeeds. - If that fails, reject the login attempt.
Note: Redmine will make a note of which source successfully authenticated a specific user. That source will be tried first the next time that user tries to login. Administrators can manually set/override that on a user-by-user basis via Administration -> Users -> {user} -> Authentication mode
.
Implementing An Alternative Authentication Source¶
This article assumes the alternative authentication involves querying a table (or tables) in some other database. However, the approach can be generalized to authenticate against pretty much anything else (some secret file, some network service, whatever fun scenario you have); you may want to examine the Redmine code in app/models/auth_source_ldap.rb
for a non-database alternative authentication example.
Insert a Sensible auth_sources
Record¶
First, we should decide what our auth_sources
record will look like. Redmine core and our AuthSource
subclass code will make use of that info, so it's good to figure this out up front.
For reference, here is the (Redmine 2.4.1) auth_sources
table:
+-------------------+--------------+------+-----+---------+----------------+ | Field | Type | Null | Key | Default | Extra | +-------------------+--------------+------+-----+---------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | type | varchar(30) | NO | | | | | name | varchar(60) | NO | | | | | host | varchar(60) | YES | | NULL | | | port | int(11) | YES | | NULL | | | account | varchar(255) | YES | | NULL | | | account_password | varchar(255) | YES | | | | | base_dn | varchar(255) | YES | | NULL | | | attr_login | varchar(30) | YES | | NULL | | | attr_firstname | varchar(30) | YES | | NULL | | | attr_lastname | varchar(30) | YES | | NULL | | | attr_mail | varchar(30) | YES | | NULL | | | onthefly_register | tinyint(1) | NO | | 0 | | | tls | tinyint(1) | NO | | 0 | | | filter | varchar(255) | YES | | NULL | | | timeout | int(11) | YES | | NULL | | +-------------------+--------------+------+-----+---------+----------------+
This schema has been relatively stable, although older Redmine versions may be missing the last 2 columns (e.g. Redmine 0.9 doesn't have them).
How ourAuthSource
subclass code will make use of these fields is more-or-less up to us, but there are two key constraints from Redmine:
type
must be the name of yourAuthSource
subclass- Redmine will use this field to instantiate your class and call its
authenticate()
method when attempting to authenticate a login attempt using your custom source. - This class name should begin with
AuthSource
. - We'll put "
AuthSourceMyCustomApp
"
- Redmine will use this field to instantiate your class and call its
onthefly_register has a 1 or 0
- Redmine will use this field to determine if unknown users (logins Redmine doesn't know about yet) can be registered within Redmine using this authentication source. Otherwise, if you put "0" here, an Administrator will first have to register the user manually (and presumably set their
Authentication mode
)--Redmine won't add them automatically.
- Redmine will use this field to determine if unknown users (logins Redmine doesn't know about yet) can be registered within Redmine using this authentication source. Otherwise, if you put "0" here, an Administrator will first have to register the user manually (and presumably set their
Here's how we'll use these fields (substitute your own values):
Field | Our Value | Comment |
id | NULL |
Let the database engine provide the id. |
type | "AuthSourceMyCustomApp" | Name of your AuthSource subclass |
name | "MyCustomApp" | Name of this alternative authentication source. Will be displayed in Administration UI pages. |
host | "myApp.other.host.edu" | Host name where the other database lives. This article doesn't assume that's the same host as where your Redmine database is. |
port | 3306 | Port for the database on that other host. |
account | "myDbUser" | Account name for accessing that other database. |
account_password | "myDbPass" | Password for that account for accessing the other database. |
base_dn | "mysql:myApp" | This field sounds very LDAP-ish. Sorry. We will interpret it to mean "BASic Database Name data" and store within a string of the form "{dbAdapterName}:{dbName} ". |
attr_login | "name" | What field in your other database table contains the login? |
attr_firstname | "firstName" | What field in your other database table contains the user's first name? |
attr_lastname | "lastName" | What field in your other database table contains the user's last name? |
attr_mail | "email" | What field in your other database table contains the user's email? |
onthefly_register | 1 | Yes, if this source authenticates the user then Redmine should create an internal record for them (w/o password info). |
tls | 0 | Dunno. 0 for "no". |
filter | NULL | Dunno. NULL is the default though. |
timeout | NULL | Dunno. Timeout while waiting for alternative authenticator? NULL is the default though. |
Note: The attr_*
fields are not always needed. They are used by the LDAP authentication source to map LDAP attributes to Redmine attributes. I recommend using them, however, since they make the authentication()
code more widely applicable (fewer changes necessary for you to use the code in your specific situation).
So we insert the record into our Redmine's (v2.4.1) auth_sources
table with SQL like the following:
INSERT INTO auth_sources VALUES (NULL, 'AuthSourceMyCustomApp', 'MyCustomApp', 'myApp.other.host.edu', 3306, 'myDbUser', 'myDbPass', 'mysql:myApp', 'name', 'firstName', 'lastName', 'email', 1, 0, null, null)
Implement Your AuthSource
Subclass¶
Create a new file for your AuthSource
subclass in app/models/
, following the naming scheme of the existing auth_source.rb
and auth_source_ldap.rb
.
- Here we'll use
app/models/auth_source_myApp.rb
Implement the class such that a call to its authenticate()
method will contact the other database and use the table there to check the provided login credentials. The values in your auth_sources
table record above are available via instances variables (e.g. self.host
, self.base_dn
, self.attr_firstname
).
Here's our commented class:
# Redmine MyApp Authentication Source # # Copyright (C) 2010 Andrew R Jackson # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Let's have a new class for our ActiveRecord-based connection # to our alternative authentication database. Remember that we're # not assuming that the alternative authentication database is on # the same host (and/or port) as Redmine's database. So its current # database connection may be of no use to us. ActiveRecord uses class # variables to store state (yay) like current connections and such; thus, # dedicated class... class MyAppCustomDB_ActiveRecord < ActiveRecord::Base PAUSE_RETRIES = 5 MAX_RETRIES = 50 end # Subclass AuthSource class AuthSourceMyCustomApp < AuthSource # authentication() implementation # - Redmine will call this method, passing the login and password entered # on the Sign In form. # # +login+ : what user entered for their login # +password+ : what user entered for their password def authenticate(login, password) retVal = nil unless(login.blank? or password.blank?) # Get a connection to the authenticating database. # - Don't use ActiveRecord::Base when using establish_connection() to get at # your alternative database (leave Redmine's current connection alone). # Use class you prepped above. # - Recall that the values stored in the fields of your auth_sources # record are available as self.fieldName # First, get the DB Adapter name and database to use for connecting: adapter, dbName = self.base_dn.split(':') # Second, try to get a connection, safely dealing with the MySQL<->ActiveRecord # failed connection bug that can still arise to this day (regardless of # reconnect, oddly). retryCount = 0 begin connPool = MyAppCustomDB_ActiveRecord.establish_connection( :adapter => adapter, :host => self.host, :port => self.port, :username => self.account, :password => self.account_password, :database => dbName, :reconnect => true ) db = connPool.checkout() rescue => err # for me, always due to dead connection; must retry bunch-o-times to get a good one if this happens if(retryCount < MyAppCustomDB_ActiveRecord::MAX_RETRIES) sleep(1) if(retryCount < MyAppCustomDB_ActiveRecord::PAUSE_RETRIES) retryCount += 1 connPool.disconnect! retry # start again at begin else # too many retries, serious, reraise error and let it fall through as it normally would in Rails. raise end end # Third, query the alternative authentication database for needed info. SQL # sufficient, obvious, and doesn't require other setup/LoC. Even more the # case if we have our database engine compute our digests (here, the whole # username is a salt). SQL also nice if your alt auth database doesn't have # AR classes and is not part of a Rails app, etc. resultRow = db.select_one( "SELECT #{self.attr_login}, #{self.attr_firstname}, #{self.attr_lastname}, #{self.attr_mail} " + "FROM genboreeuser " + "WHERE SHA1(CONCAT(#{self.attr_login}, password)) = SHA1(CONCAT('#{db.quote_string(login)}', '#{db.quote_string(password)}'))" ) unless(resultRow.nil? or resultRow.empty?) user = resultRow[self.attr_login] unless(user.nil? or user.empty?) # Found a record whose login & password digest matches that computed # from Sign Inform parameters. If allowing Redmine to automatically # register such accounts in its internal table, return account # information to Redmine based on record found. retVal = { :firstname => resultRow[self.attr_firstname], :lastname => resultRow[self.attr_lastname], :mail => resultRow[self.attr_mail], :auth_source_id => self.id } if(onthefly_register?) end end end # Check connection back into pool. connPool.checkin(db) return retVal end def auth_method_name "MyCustomApp" end end
Deploy & Test¶
Save your new class in app/model/
and restart Redmine.
- You ought to be able to try to log in as a use that exists in your alternative database but that doesn't exist in your Redmine instance.
- Existing Redmine accounts (and passwords) should continue to work.
- If you examine Redmine's
users
table, you should see records appear after each successful login that used your alternative database.hashed_password
will be empty for those records.auth_source_id
will have theid
fromauth_sources
which worked to authenticate the user;NULL
means "use internal Redmine authentication". The Administrator can also manually set this value via theAuthentication mode
UI widget I mentioned above.
- Users authenticated with an alternative source will not be able to change their passwords using Redmine (great check by the core code) and will see an error message if they try to do so.
Follow-Up¶
If you want to ONLY use your alternative authentication source for Redmine Sign In, remove the "Register" button. We did this by removing the menu.push :register
line in lib/redmine.rb
. And we turned off the "Lost Password" feature via Administration -> Settings -> Authentication -> Lost password
.
This was all pretty fast and simple to set up, thanks to how Redmine is organized and that some thought about permitting this kind of thing had been made. Quality. I hope I didn't get anything too wrong.
Joomla¶
We've customized the code to integrate Redmine with Joomla. Please, check the attachment to review an implementation for Joomla 2.5 and Redmine 2.0.3.
As Joomla does not have lastname in the users table, we have added a Html tag to show an icon in Redmine.
Remember to change the DB prefix (line 84) and the lastname customization (line 98).
Bugs¶
- In 1.0.1 if you got an error like "undefined method `stringify_keys!' for #<Array:...>) when logging" in see #6196
# Lines 97 from previous script retVal = { :firstname => resultRow[self.attr_firstname], :lastname => resultRow[self.attr_lastname], :mail => resultRow[self.attr_mail], :auth_source_id => self.id } if(onthefly_register?) # ...
- In 2.0.3, the codefix for #6196 is still required, we've fixed the original code.
Updated by Andrew R Jackson over 10 years ago · 10 revisions