Documentation ¶
Overview ¶
Package Authaus is an authentication and authorization system.
Authaus brings together the following pluggable components:
Authenticator This simply answers the question "Is this username/password valid?" Session Database This stores session keys and associated tokens (aka cookies). Permit Database This is where you store the permits (aka permissions granted). Role Groups Database This knows how to interpret a permit, and turn it into a list of roles. User Store This is where we store user details, such as email address, contact details, name, surname etc.
Any of these five components can be swapped out, and in fact the fourth, and fifth ones (Role Groups and User Store) are entirely optional.
A typical setup is to use LDAP as an Authenticator, and Postgres as a Session, Permit, and Role Groups database.
Your session database does not need to be particularly performant, since Authaus maintains an in-process cache of session keys and their associated tokens.
Intended Usage ¶
Authaus was NOT designed to be a "Facebook Scale" system. The target audience is a system of perhaps 100,000 users. There is nothing fundamentally limiting about the API of Authaus, but the internals certainly have not been built with millions of users in mind.
The intended usage model is this:
Authaus is intended to be embedded inside your security system, and run as a standalone HTTP service (aka a REST service). This HTTP service CAN be open to the wide world, but it's also completely OK to let it listen only to servers inside your DMZ. Authaus only gives you the skeleton and some examples of HTTP responders. It's up to you to flesh out the details of your authentication HTTP interface, and whether you'd like that to face the world, or whether it should only be accessible via other services that you control.
At startup, your services open an HTTP connection to the Authaus service. This connection will typically live for the duration of the service. For every incoming request, you peel off whatever authentication information is associated with that request. This is either a session key, or a username/password combination. Let's call it the authorization information. You then ask Authaus to tell you WHO this authorization information belongs to, as well as WHAT this authorization information allows the requester to do (ie Authentication and Authorization). Authaus responds either with a 401 (Unauthorized), 403 (Forbidden), or a 200 (OK) and a JSON object that tells you the identity of the agent submitting this request, as well the permissions that this agent posesses. It's up to your individual services to decide what to do with that information.
It should be very easy to expose Authaus over a protocol other than HTTP, since Authaus is intended to be easy to embed. The HTTP API is merely an illustrative example.
Concepts ¶
A `Session Key` is the long random number that is typically stored as a cookie.
A `Permit` is a set of roles that has been granted to a user. Authaus knows nothing about the contents of a permit. It simply treats it as a binary blob, and when writing it to an SQL database, encodes it as base64. The interpretation of the permit is application dependent. Typically, a Permit will hold information such as "Allowed to view billing information", or "Allowed to paint your bathroom yellow". Authaus does have a built-in module called RoleGroupDB, which has its own interpretation of what a Permit is, but you do not need to use this.
A `Token` is the result of a successful authentication. It stores the identity of a user, an expiry date, and a Permit. A token will usually be retrieved by a session key. However, you can also perform a once-off authentication, which also yields you a token, which you will typically throw away when you are finished with it.
Concurrency ¶
All public methods of the `Central` object are callable from multiple threads. Reader-Writer locks are used in all of the caching systems.
The number of concurrent connections is limited only by the limits of the Go runtime, and the performance limits that are inherent to the simple reader-writer locks used to protect shared state.
Deployment ¶
Authaus must be deployed as a single process (which implies running on a single logical machine). The sole reason why it must run on only one process and not more, is because of the state that lives inside the various Authaus caches. Were it not for these caches, then there would be nothing preventing you from running Authaus on as many machines as necessary.
The cached state stored inside the Authaus server is:
- Cached Session Database
- Cached Role Group Database
If you wanted to make Authaus runnable across multiple processes, then you would need to implement a cache invalidation system for these caches.
DOS Attacks ¶
Authaus makes no attempt to mitigate DOS attacks. The most sane approach in this domain seems to be this (http://security.stackexchange.com/questions/12101/prevent-denial-of-service-attacks-against-slow-hashing-functions).
Crypto ¶
The password database (created via NewAuthenticationDB_SQL) stores password hashes using the scrypt key derivation system (http://www.tarsnap.com/scrypt.html).
Internally, we store our hash in a format that can later be extended, should we wish to double-hash the passwords, etc. The hash is 65 bytes and looks like this:
Bytes 1 32 32 (sum = 65 bytes) Information Version Salt Hash
The first byte of the hash is a version number of the hash. The remaining 64 bytes are the salt and the hash itself. At present, only one version is supported, which is version 1. It consists of 32 bytes of salt, and 32 bytes of scrypt'ed hash, with scrypt parameters N=256 r=8 p=1. Note that the parameter N=256 is quite low, meaning that it is possible to compute this in approximately 1 millisecond (1,000,000 nanoseconds) on a 2009-era Intel Core i7. This is a deliberate tradeoff. On the same CPU, a SHA256 hash takes about 500 nanoseconds to compute, so we are still making it 2000 times harder to brute force the passwords than an equivalent system storing only a SHA256 salted hash. This discussion is only of relevance in the event that the password table is compromised.
No cookie signing mechanism is implemented.
Cookies are not presently transmitted with Secure:true. This must change.
LDAP Authenticator ¶
The LDAP Authenticator is extremely simple, and provides only one function: Authenticate a user against an LDAP system (often this means Active Directory, AKA a Windows Domain).
It calls the LDAP "Bind" method, and if that succeeds for the given identity/password, then the user is considered authenticated.
We take care not to allow an "anonymous bind", which many LDAP servers allow when the password is blank.
Session Database ¶
The Session Database runs on Postgres. It stores a table of sessions, where each row contains the following information:
- A session key (aka the cookie's "Value")
- The identity that created that session
- The cached permit of that identity
- When the session expires
When a permit is altered with Authaus, then all existing sessions have their permits altered transparently. For example, imagine User X is logged in, and his administrator grants him a new permission. User X does not need to log out and log back in again in order for his new permissions to be reflected. His new permissions will be available immediately.
Similarly, if a password is changed with Authaus, then all sessions are invalidated. Do take note though, that if a password is changed through an external mechanism (such as with LDAP), then Authaus will have no way of knowing this, and will continue to serve up sessions that were authenticated with the old password. This is a problem that needs addressing.
You can limit the number of concurrent sessions per user to 1, by setting MaxActiveSessions.ConfigSessionDB to 1. This setting may only be zero or one. Zero, which is the default, means an unlimited number of concurrent sessions per user.
Session Cache ¶
Authaus will always place your Session Database behind its own Session Cache. This session cache is a very simple single-process in-memory cache of recent sessions. The limit on the number of entries in this cache is hard-coded, and that should probably change.
Permit Database ¶
The Permit database runs on Postgres. It stores a table of permits, which is simply a 1:1 mapping from Identity -> Permit. The Permit is just an array of bytes, which we store base64 encoded, inside a text field. This part of the system doesn't care how you interpret that blob.
Role Group Database ¶
The Role Group Database is an entirely optional component of Authaus. The other components of Authaus (Authenticator, PermitDB, SessionDB) do not understand your Permits. To them, a Permit is simply an arbitrary array of bytes.
The Role Group Database is a component that adds a specific meaning to a permit blob. Let's see what that specific meaning looks like...
The built-in Role Group Database interprets a permit blob as a string of 32-bit integer IDs:
// A permit with 3 "role groups" 0x000000bc 0x00000001 0x000000fe
These 32-bit integer IDs refer to "role groups" inside a database table. The "role groups" table might look like this:
------------------------------------ Role Groups Table ------------------------------------ ID Roles 0x00000001 0x00ab 0x00aa 0x0001 0x000000bc 0x00b0 0x000000fe 0x00b0 0x00bf 0x0001
The Role Group IDs use 32-bit indices, because we assume that you are not going to create more than 2^32 different role groups. The worst case we assume here is that of an automated system that creates 100,000 roles per day. Such a system would run for more than 100 years, given a 32-bit ID. These constraints are extraordinary, suggesting that we do not even need 32 bits, but could even get away with just a 16-bit group ID.
However, we expect the number of groups to be relatively small. Our aim here, arbitrary though it may be, is to fit the permit and identity into a single ethernet packet, which one can reasonably peg at 1500 bytes. 1500 / 4 = 375. We assume that no sane human administrator will assign 375 security groups to any individual. We expect the number of groups assigned to any individual to be in the range of 1 to 20. This makes 375 a gigantic buffer.
OAuth ¶
OAuth support in Authaus is limited to a very simple scenario:
* You wish to allow your users to login using an OAuth service - thereby outsourcing the Authentication to that external service, and using it to populate the email address of your users.
OAuth was developed in order to work with Microsoft Azure Active Directory, however it should be fairly easy to extend the code to be able to handle other OAuth providers.
Inside the database are two tables related to OAuth:
oauthchallenge: The challenge table holds OAuth sessions which have been started, and which are expected to either succeed or fail within the next few minutes. The default timeout for a challenge is 5 minutes. A challenge record is usually created the moment the user clicks on the "Sign in with Microsoft" button on your site, and it tracks that authentication attempt.
oauthsession: The session table holds OAuth sessions which have successfully authenticated, and also the token that was retrieved by a successful authorization. If a token has expired, then it is refreshed and updated in-place, inside the oauthsession table.
An OAuth login follows this sequence of events:
1. User clicks on a "Signin with X" button on your login page 2. A record is created in the oauthchallenge table, with a unique ID. This ID is a secret known only to the authaus server and the OAuth server. It is used as the `state` parameter in the OAuth login mechanism. 3. The HTTP call which prompts #2 return a redirect URL (eg via an HTTP 302 response), which redirects the user's browser to the OAuth website, so that the user can either grant or refuse access. If the user refuses, or fails to login, then the login sequence ends here. 4. Upon successful authorization with the OAuth system, the OAuth website redirects the user back to your website, to a URL such as example.com/auth/oauth/finish, and you'll typically want Authaus to handle this request directly (via HttpHandlerOAuthFinish). Authaus will extract the secrets from the URL, perform any validations necessary, and then move the record from the oauthchallenge table, into the oauthsession table. While 'moving' the record over, it will also add any additional information that was provided by the successful authentication, such as the token provided by the OAuth provider. 5. Authaus makes an API call to the OAuth system, to retrieve the email address and name of the person that just logged in, using the token just received. 6. If that email address does not exist inside authuserstore, then create a new user record for this identity. 7. Log the user into Authaus, by creating a record inside authsession, for the relevant identity. Inside the authsession table, store a link to the oauthsession record, so that there is a 1:1 link from the authsession table, to the oauthsession table (ie Authaus Session to OAuth Token). 8. Return an Authaus session cookie to the browser, thereby completing the login.
Although we only use our OAuth token a single time, during login, to retrieve the user's email address and name, we retain the OAuth token, and so we maintain the ability to make other API calls on behalf of that user. This hasn't proven necessary yet, but it seems like a reasonable bit of future-proofing.
Testing ¶
See the guidelines at the top of all_test.go for testing instructions.
Index ¶
- Constants
- Variables
- func CanonicalizeIdentity(identity string) string
- func DeleteGroup(roleDB RoleGroupDB, groupName string) error
- func EncodePermit(groupIds []GroupIDU32) []byte
- func GroupIDsToNames(groups []GroupIDU32, db RoleGroupDB) ([]string, error)
- func GroupNameIsLegal(name string) bool
- func HttpHandlerLogin(config *ConfigHTTP, central *Central, w http.ResponseWriter, r *http.Request)
- func HttpHandlerLogout(config *ConfigHTTP, central *Central, w http.ResponseWriter, r *http.Request)
- func HttpHandlerWhoAmI(config *ConfigHTTP, central *Central, w http.ResponseWriter, r *http.Request)
- func HttpNoCache(w http.ResponseWriter)
- func HttpSendHTML(w http.ResponseWriter, responseCode int, responseBody string)
- func HttpSendJSON(w http.ResponseWriter, responseCode int, responseObject interface{})
- func HttpSendTxt(w http.ResponseWriter, responseCode int, responseBody string)
- func NewError(base error, detail string) error
- func NewLDAPConnect(config *ConfigLDAP) (*ldap.LDAPConnection, error)
- func NewLDAPConnectAndBind(config *ConfigLDAP) (*ldap.LDAPConnection, error)
- func RandomString(nchars int, corpus string) string
- func RunHttp(config *ConfigHTTP, central *Central) error
- func RunHttpFromConfig(config *Config) error
- func RunMigrations(conx *DBConnection) error
- func SqlCreateDatabase(conx *DBConnection) error
- type AuditActionType
- type Auditor
- type AuthCheck
- type AuthGroup
- type AuthUser
- type AuthUserType
- type Central
- func (x *Central) ArchiveIdentity(userId UserId) error
- func (x *Central) AuthenticateUser(identity, password string, authTypeCheck AuthCheck) error
- func (x *Central) Close()
- func (x *Central) CreateSession(user *AuthUser, clientIPAddress string) (sessionkey string, token *Token, err error)
- func (x *Central) CreateUserStoreIdentity(user *AuthUser, password string) (UserId, error)
- func (x *Central) GetAuthenticatorIdentities(getIdentitiesFlag GetIdentitiesFlag) ([]AuthUser, error)
- func (x *Central) GetPermit(userId UserId) (*Permit, error)
- func (x *Central) GetPermits() (map[UserId]*Permit, error)
- func (x *Central) GetRoleGroupDB() RoleGroupDB
- func (x *Central) GetTokenFromIdentityPassword(identity, password string) (*Token, error)
- func (x *Central) GetTokenFromSession(sessionkey string) (*Token, error)
- func (x *Central) GetUserFromIdentity(identity string) (AuthUser, error)
- func (x *Central) GetUserFromUserId(userId UserId) (AuthUser, error)
- func (x *Central) GetUserNameFromUserId(userId UserId) string
- func (x *Central) InvalidateSessionsForIdentity(userId UserId) error
- func (x *Central) Login(username, password string, clientIPAddress string) (sessionkey string, token *Token, err error)
- func (x *Central) Logout(sessionkey string) error
- func (x *Central) MergeLdapUsersIntoLocalUserStore(ldapUsers []AuthUser, imqsUsers []AuthUser)
- func (x *Central) MergeTick()
- func (x *Central) RenameIdentity(oldIdent, newIdent string) error
- func (x *Central) ResetPasswordFinish(userId UserId, token string, password string) error
- func (x *Central) ResetPasswordStart(userId UserId, expires time.Time) (string, error)
- func (x *Central) SetPassword(userId UserId, password string) error
- func (x *Central) SetPermit(userId UserId, permit *Permit) error
- func (x *Central) SetSessionCacheSize(maxSessions int)
- func (x *Central) StartMergeTicker() error
- func (x *Central) UnlockAccount(userId UserId) error
- func (x *Central) UpdateIdentity(user *AuthUser) error
- type CentralStats
- func (x *CentralStats) IncrementAndLog(name string, val *uint64, logger *log.Logger)
- func (x *CentralStats) IncrementEmptyIdentities(logger *log.Logger)
- func (x *CentralStats) IncrementExpiredSessionKey(logger *log.Logger)
- func (x *CentralStats) IncrementGoodLogin(logger *log.Logger)
- func (x *CentralStats) IncrementGoodOnceOffAuth(logger *log.Logger)
- func (x *CentralStats) IncrementInvalidPasswordHistory(logger *log.Logger, username string, clientIPAddress string)
- func (x *CentralStats) IncrementInvalidPasswords(logger *log.Logger)
- func (x *CentralStats) IncrementInvalidSessionKey(logger *log.Logger)
- func (x *CentralStats) IncrementLogout(logger *log.Logger)
- func (x *CentralStats) ResetInvalidPasswordHistory(logger *log.Logger, username string, clientIPAddress string)
- type Config
- type ConfigHTTP
- type ConfigLDAP
- type ConfigLog
- type ConfigOAuth
- type ConfigOAuthProvider
- type ConfigSessionDB
- type ConfigUserStoreDB
- type DBConnection
- type GetIdentitiesFlag
- type GroupIDU32
- type LDAP
- type LdapConnectionMode
- type LdapImpl
- type LockingPolicy
- type OAuth
- func (x *OAuth) HttpHandlerOAuthFinish(w http.ResponseWriter, r *http.Request)
- func (x *OAuth) HttpHandlerOAuthStart(w http.ResponseWriter, r *http.Request)
- func (x *OAuth) HttpHandlerOAuthTest(w http.ResponseWriter, r *http.Request)
- func (x *OAuth) Initialize(parent *Central)
- func (x *OAuth) OAuthFinish(r *http.Request) (*OAuthCompletedResult, error)
- type OAuthCompletedResult
- type PasswordEnforcement
- type PermissionList
- type PermissionNameTable
- type PermissionU16
- type Permit
- type PermitDB
- type RoleGroupCache
- func (x *RoleGroupCache) Close()
- func (x *RoleGroupCache) DeleteGroup(group *AuthGroup) (err error)
- func (x *RoleGroupCache) GetByID(id GroupIDU32) (*AuthGroup, error)
- func (x *RoleGroupCache) GetByName(name string) (*AuthGroup, error)
- func (x *RoleGroupCache) GetGroups() ([]*AuthGroup, error)
- func (x *RoleGroupCache) InsertGroup(group *AuthGroup) (err error)
- func (x *RoleGroupCache) UpdateGroup(group *AuthGroup) (err error)
- type RoleGroupDB
- type SessionDB
- type Token
- type UserId
- type UserStore
Constants ¶
const ( AuditActionAuthentication AuditActionType = "Login" AuditActionCreated = "Created" AuditActionUpdated = "Updated" AuditActionDeleted = "Deleted" AuditActionResetPassword = "Reset Password" AuditActionFailedLogin = "Failed Login" AuditActionUnlocked = "User Account Unlocked" AuditActionLocked = "User Account Locked" )
const ( LdapConnectionModePlainText LdapConnectionMode = iota LdapConnectionModeSSL = iota LdapConnectionModeTLS = iota )
const DefaultOAuthLoginExpirySeconds = 5 * 60
const (
OAuthProviderMSAAD = "msaad" // Microsoft Azure Active Directory
)
Variables ¶
var ( // NOTE: These 'base' error strings may not be prefixes of each other, // otherwise it violates our NewError() concept, which ensures that // any Authaus error starts with one of these *unique* prefixes ErrConnect = errors.New("Connect failed") ErrUnsupported = errors.New("Unsupported operation") ErrIdentityAuthNotFound = errors.New("Identity authorization not found") ErrIdentityPermitNotFound = errors.New("Identity permit not found") ErrIdentityEmpty = errors.New("Identity may not be empty") ErrIdentityExists = errors.New("Identity already exists") // We should perhaps keep a consistent error, like ErrInvalidCredentials throught the app, as it can be a security risk returning InvalidPassword to a user that may be malicious ErrInvalidPassword = errors.New("Invalid password") ErrAccountLocked = errors.New("Account locked. Please contact your administrator") ErrInvalidSessionToken = errors.New("Invalid session token") ErrInvalidPasswordToken = errors.New("Invalid password token") ErrPasswordTokenExpired = errors.New("Password token has expired") ErrPasswordExpired = errors.New("Password has expired") ErrInvalidPastPassword = errors.New("Invalid previously used password") ErrInvalidCredentials = errors.New("Invalid Credentials") // This error was created for LDAP authentication. LDAP does not return 'identity not found' or 'invalid password' but simply invalid credentials )
var ( ErrHttpBasicAuth = errors.New("HTTP Basic Authorization must be base64(identity:password)") ErrHttpNotAuthorized = errors.New("No authorization information") )
var ( ErrGroupNotExist = errors.New("Group does not exist") ErrGroupExists = errors.New("Group already exists") ErrGroupNameIllegal = errors.New("Group name may not be empty, and must not have spaces on the left or right") ErrGroupDuplicateName = errors.New("A group with that name already exists") ErrPermitInvalid = errors.New("Permit is not a sequence of 32-bit words") )
Functions ¶
func CanonicalizeIdentity ¶
Transform an identity into its canonical form. What this means is that any two identities are considered equal if their canonical forms are equal. This is simply a lower-casing of the identity, so that "bob@enterprise.com" is equal to "Bob@enterprise.com". It also trims the whitespace around the identity.
func DeleteGroup ¶
func DeleteGroup(roleDB RoleGroupDB, groupName string) error
func EncodePermit ¶
func EncodePermit(groupIds []GroupIDU32) []byte
Encodes a list of Group IDs into a Permit
func GroupIDsToNames ¶
func GroupIDsToNames(groups []GroupIDU32, db RoleGroupDB) ([]string, error)
Converts group IDs to names
func GroupNameIsLegal ¶
func HttpHandlerLogin ¶
func HttpHandlerLogin(config *ConfigHTTP, central *Central, w http.ResponseWriter, r *http.Request)
HttpHandlerLogin handles the 'login' request, sending back a session token (via Set-Cookie), if authentication succeeds. You may want to use this as a template to write your own.
func HttpHandlerLogout ¶
func HttpHandlerLogout(config *ConfigHTTP, central *Central, w http.ResponseWriter, r *http.Request)
func HttpHandlerWhoAmI ¶
func HttpHandlerWhoAmI(config *ConfigHTTP, central *Central, w http.ResponseWriter, r *http.Request)
HttpHandlerWhoAmI handles the 'whoami' request, which is really just for debugging
func HttpNoCache ¶ added in v1.0.3
func HttpNoCache(w http.ResponseWriter)
func HttpSendHTML ¶ added in v1.0.3
func HttpSendHTML(w http.ResponseWriter, responseCode int, responseBody string)
func HttpSendJSON ¶ added in v1.0.3
func HttpSendJSON(w http.ResponseWriter, responseCode int, responseObject interface{})
func HttpSendTxt ¶
func HttpSendTxt(w http.ResponseWriter, responseCode int, responseBody string)
func NewError ¶
Use this whenever you return an Authaus error. We rely upon the prefix of the error string to identify the broad category of the error.
func NewLDAPConnect ¶
func NewLDAPConnect(config *ConfigLDAP) (*ldap.LDAPConnection, error)
func NewLDAPConnectAndBind ¶
func NewLDAPConnectAndBind(config *ConfigLDAP) (*ldap.LDAPConnection, error)
func RandomString ¶
RandomString returns a random string of 'nchars' bytes, sampled uniformly from the given corpus of byte characters.
func RunHttp ¶
func RunHttp(config *ConfigHTTP, central *Central) error
Run as a standalone HTTP server. This just wires up the various HTTP handler functions and starts a listener. You will probably want to add your own entry points and do that yourself instead of using this. This function is useful for demo/example purposes.
func RunHttpFromConfig ¶
func RunMigrations ¶
func RunMigrations(conx *DBConnection) error
func SqlCreateDatabase ¶
func SqlCreateDatabase(conx *DBConnection) error
Types ¶
type AuditActionType ¶
type AuditActionType string
type Auditor ¶
type Auditor interface {
AuditUserAction(identity, item, context string, auditActionType AuditActionType)
}
type AuthGroup ¶
type AuthGroup struct { ID GroupIDU32 // DB-generated id Name string // Administrators need this name to keep sense of things. Example of this is "finance" or "engineering". PermList PermissionList // Application-defined permission bits (ie every value from 0..65535 pertains to one particular permission) }
An Authorization Group. This stores a list of permissions.
func LoadOrCreateGroup ¶
func LoadOrCreateGroup(roleDB RoleGroupDB, groupName string, createIfNotExist bool) (*AuthGroup, error)
func (*AuthGroup) AddPerm ¶
func (x *AuthGroup) AddPerm(perm PermissionU16)
This is a no-op if the permission is already set
func (*AuthGroup) HasPerm ¶
func (x *AuthGroup) HasPerm(perm PermissionU16) bool
func (*AuthGroup) RemovePerm ¶
func (x *AuthGroup) RemovePerm(perm PermissionU16)
This is a no-op if the permission is not set
type AuthUser ¶
type AuthUser struct { UserId UserId `json:"userID"` Email string `json:"email"` Username string `json:"userName"` Firstname string `json:"firstName"` Lastname string `json:"lastName"` Mobilenumber string `json:"mobileNumber"` Telephonenumber string `json:"telephoneNumber` Remarks string `json:"remarks"` Created time.Time `json:"created"` CreatedBy UserId `json:"createdBy"` Modified time.Time `json:"modified"` ModifiedBy UserId `json:"modifiedBy"` Type AuthUserType `json:"type"` Archived bool `json:"archived"` ExternalUUID string `json:"externalUUID"` PasswordModifiedDate time.Time `json:"passwordModifiedDate"` AccountLocked bool `json:"accountLocked"` }
type AuthUserType ¶
type AuthUserType int
const ( UserTypeDefault AuthUserType = 0 UserTypeLDAP AuthUserType = 1 UserTypeOAuth AuthUserType = 2 )
These constants are embedded inside our database (in the table AuthUserStore). They may never change.
func (AuthUserType) CanRenameIdentity ¶
func (u AuthUserType) CanRenameIdentity() bool
func (AuthUserType) CanSetPassword ¶
func (u AuthUserType) CanSetPassword() bool
type Central ¶
type Central struct { // Stats must be first so that we are guaranteed to get it 8-byte aligned. We atomically // increment counters inside CentralStats, and the atomic functions need 8-byte alignment // on their operands. Stats CentralStats Auditor Auditor LockingPolicy LockingPolicy Log *log.Logger MaxActiveSessions int32 NewSessionExpiresAfter time.Duration DisablePasswordReuse bool PasswordExpiresAfter time.Duration MaxFailedLoginAttempts int // only applies if EnableAccountLocking is true EnableAccountLocking bool OAuth OAuth DB *sql.DB // contains filtered or unexported fields }
For lack of a better name, this is the single hub of authentication that you interact with. All public methods of Central are callable from multiple threads.
func NewCentral ¶
func NewCentral(logfile string, ldap LDAP, userStore UserStore, permitDB PermitDB, sessionDB SessionDB, roleGroupDB RoleGroupDB) *Central
Create a new Central object from the specified pieces. roleGroupDB may be nil
func NewCentralDummy ¶
func NewCentralFromConfig ¶
Create a new 'Central' object from a Config.
func (*Central) ArchiveIdentity ¶
Archive a user in the AuthUserStore.
func (*Central) AuthenticateUser ¶
func (*Central) CreateSession ¶ added in v1.0.3
func (x *Central) CreateSession(user *AuthUser, clientIPAddress string) (sessionkey string, token *Token, err error)
CreateSession creates a new login session, after you have authenticated the caller Returns a session key, which can be used in future to retrieve the token. The internal session expiry is controlled with NewSessionExpiresAfter. The session key is typically sent to the client as a cookie.
func (*Central) CreateUserStoreIdentity ¶
Create an identity in the AuthUserStore.
func (*Central) GetAuthenticatorIdentities ¶
func (x *Central) GetAuthenticatorIdentities(getIdentitiesFlag GetIdentitiesFlag) ([]AuthUser, error)
GetAuthenticatorIdentities retrieves all identities known to the Authenticator.
func (*Central) GetPermits ¶
GetPermits retrieves all Permits.
func (*Central) GetRoleGroupDB ¶
func (x *Central) GetRoleGroupDB() RoleGroupDB
GetRoleGroupDB retrieves the Role Group Database (which may be nil)
func (*Central) GetTokenFromIdentityPassword ¶
Perform a once-off authentication
func (*Central) GetTokenFromSession ¶
Pass in a session key that was generated with a call to Login(), and get back a token. A session key is typically a cookie.
func (*Central) GetUserFromIdentity ¶
GetUserFromIdentity gets AuthUser object from identity.
func (*Central) GetUserFromUserId ¶
GetUserFromUserId gets AuthUser object from userid.
func (*Central) GetUserNameFromUserId ¶
GetUserNameFromUserId gets AuthUser full name from userid.
func (*Central) InvalidateSessionsForIdentity ¶
Invalidate all sessions for a particular identity
func (*Central) Login ¶
func (x *Central) Login(username, password string, clientIPAddress string) (sessionkey string, token *Token, err error)
Authenticate the username + password, and if successful, call CreateSession()
func (*Central) MergeLdapUsersIntoLocalUserStore ¶
We are reading users from LDAP/AD and merging them into the IMQS userstore
func (*Central) RenameIdentity ¶
Rename an identity. Invalidates all existing sessions for that identity
func (*Central) ResetPasswordFinish ¶
Complete the password reset process, by providing a token that was generated by ResetPasswordStart. If this succeeds, then the password is set to 'password', and the token becomes invalid.
func (*Central) ResetPasswordStart ¶
Create a one-time token that can be used to reset the password with a subsequent call to ResetPasswordFinish. Any subsequent call to ResetPasswordStart causes the current token to be invalidated, so there can only be a single active token. The token is valid until the time specified by 'expires'.
func (*Central) SetPassword ¶
Change a Password. This invalidates all sessions for this identity.
func (*Central) SetSessionCacheSize ¶
Set the size of the in-memory session cache
func (*Central) StartMergeTicker ¶
Merges ldap with user store every merge tick
func (*Central) UnlockAccount ¶
Unlock user in the AuthUserStore.
func (*Central) UpdateIdentity ¶
Update a user in the AuthUserStore.
type CentralStats ¶
type CentralStats struct { InvalidSessionKeys uint64 ExpiredSessionKeys uint64 InvalidPasswords uint64 EmptyIdentities uint64 GoodOnceOffAuth uint64 GoodLogin uint64 Logout uint64 UserLoginAttempts map[string]uint64 // contains filtered or unexported fields }
func (*CentralStats) IncrementAndLog ¶
func (x *CentralStats) IncrementAndLog(name string, val *uint64, logger *log.Logger)
func (*CentralStats) IncrementEmptyIdentities ¶
func (x *CentralStats) IncrementEmptyIdentities(logger *log.Logger)
func (*CentralStats) IncrementExpiredSessionKey ¶
func (x *CentralStats) IncrementExpiredSessionKey(logger *log.Logger)
func (*CentralStats) IncrementGoodLogin ¶
func (x *CentralStats) IncrementGoodLogin(logger *log.Logger)
func (*CentralStats) IncrementGoodOnceOffAuth ¶
func (x *CentralStats) IncrementGoodOnceOffAuth(logger *log.Logger)
func (*CentralStats) IncrementInvalidPasswordHistory ¶
func (x *CentralStats) IncrementInvalidPasswordHistory(logger *log.Logger, username string, clientIPAddress string)
func (*CentralStats) IncrementInvalidPasswords ¶
func (x *CentralStats) IncrementInvalidPasswords(logger *log.Logger)
func (*CentralStats) IncrementInvalidSessionKey ¶
func (x *CentralStats) IncrementInvalidSessionKey(logger *log.Logger)
func (*CentralStats) IncrementLogout ¶
func (x *CentralStats) IncrementLogout(logger *log.Logger)
func (*CentralStats) ResetInvalidPasswordHistory ¶
func (x *CentralStats) ResetInvalidPasswordHistory(logger *log.Logger, username string, clientIPAddress string)
type Config ¶
type Config struct { DB DBConnection Log ConfigLog HTTP ConfigHTTP SessionDB ConfigSessionDB LDAP ConfigLDAP UserStore ConfigUserStoreDB OAuth ConfigOAuth AuditServiceUrl string EnableAccountLocking bool MaxFailedLoginAttempts int }
Configuration information. This is typically loaded from a .json config file.
type ConfigHTTP ¶
type ConfigLDAP ¶
type ConfigLDAP struct { LdapHost string // LdapPort uint16 // Encryption string // "", "TLS", "SSL" LdapUsername string // LdapPassword string // LdapDomain string // LdapTickerTime int // seconds BaseDN string // SysAdminEmail string // LdapSearchFilter string InsecureSkipVerify bool // If true, then skip SSL verification. Only applicable when Encryption = SSL DebugUserPull bool // If true, prints out the result of every LDAP user pull }
type ConfigOAuth ¶ added in v1.0.3
type ConfigOAuth struct { Providers map[string]*ConfigOAuthProvider Verbose bool // If true, then print a lot of debugging information ForceFastTokenRefresh bool // If true, then force a token refresh every 120 seconds. This is for testing the token refresh code. LoginExpirySeconds int64 // A session that starts must be completed within this time period (eg 5 minutes) }
func (*ConfigOAuth) LoginExpiry ¶ added in v1.0.3
func (c *ConfigOAuth) LoginExpiry() time.Duration
type ConfigOAuthProvider ¶ added in v1.0.3
type ConfigOAuthProvider struct { Type string // See OAuthProvider___ constants for legal values Title string // Name of provider that user sees (probably need an image too) ClientID string // For MSAAD LoginURL string // eg https://login.microsoftonline.com/e1ff61b3-a3da-4639-ae31-c6dff3ce7bfb/oauth2/v2.0/authorize TokenURL string // eg https://login.microsoftonline.com/e1ff61b3-a3da-4639-ae31-c6dff3ce7bfb/oauth2/v2.0/token Scope string ClientSecret string }
type ConfigSessionDB ¶
type ConfigUserStoreDB ¶
type DBConnection ¶
type DBConnection struct { Driver string Host string Port uint16 Database string User string Password string SSL bool }
Database connection information
func (*DBConnection) ConnectionString ¶
func (x *DBConnection) ConnectionString() string
func (*DBConnection) Equals ¶
func (x *DBConnection) Equals(y *DBConnection) bool
type GetIdentitiesFlag ¶
type GetIdentitiesFlag int
const ( GetIdentitiesFlagNone GetIdentitiesFlag = 0 GetIdentitiesFlagDeleted GetIdentitiesFlag = 1 << (iota - 1) )
type GroupIDU32 ¶
type GroupIDU32 uint32
Our group IDs are unsigned 32-bit integers
func DecodePermit ¶
func DecodePermit(permit []byte) ([]GroupIDU32, error)
DecodePermit decodes a Permit into a list of Group IDs
func GroupNamesToIDs ¶
func GroupNamesToIDs(groups []string, db RoleGroupDB) ([]GroupIDU32, error)
Converts group names to group IDs. From here you can use EncodePermit to get a blob that is ready for use as Permit.Roles
type LDAP ¶
type LDAP interface { Authenticate(identity, password string) error // Return nil if the password is correct, otherwise one of ErrIdentityAuthNotFound or ErrInvalidPassword GetLdapUsers() ([]AuthUser, error) // Retrieve the list of users from ldap Close() // Typically used to close a database handle }
The LDAP interface allows authentication and the ability to retrieve the LDAP's users and merge them into our system
type LdapConnectionMode ¶
type LdapConnectionMode int
type LdapImpl ¶
type LdapImpl struct {
// contains filtered or unexported fields
}
func NewAuthenticator_LDAP ¶
func NewAuthenticator_LDAP(config *ConfigLDAP) *LdapImpl
func (*LdapImpl) Authenticate ¶
func (*LdapImpl) GetLdapUsers ¶
type LockingPolicy ¶
LockingPolicy controls on a per-user basis, whether that user's account is automatically locked, after a set number of failed login attempts. It was created to disable the locking of special accounts, such as administrators or internal infrastructure accounts. This only applies if EnableAccountLocking is true
type OAuth ¶ added in v1.0.3
type OAuth struct { Config ConfigOAuth // contains filtered or unexported fields }
This is just a container for OAuth functions and state, so that we don't pollute the 'Central' struct with all of our stuff.
func (*OAuth) HttpHandlerOAuthFinish ¶ added in v1.0.3
func (x *OAuth) HttpHandlerOAuthFinish(w http.ResponseWriter, r *http.Request)
This is a dummy "finish" handler. A real handler would be a function where you create a session cookie, and then redirect the user to a reasonable landing page.
func (*OAuth) HttpHandlerOAuthStart ¶ added in v1.0.3
func (x *OAuth) HttpHandlerOAuthStart(w http.ResponseWriter, r *http.Request)
This is a GET or POST request that the frontend calls, in order to start an OAuth login sequence
func (*OAuth) HttpHandlerOAuthTest ¶ added in v1.0.3
func (x *OAuth) HttpHandlerOAuthTest(w http.ResponseWriter, r *http.Request)
It's useful to keep a function like this around, so that you can iterate on this stuff without having to go through a complete login cycle every time. I'm afraid I'm going to burn up some max-logins-per-hour quota or something like that.
func (*OAuth) Initialize ¶ added in v1.0.3
func (*OAuth) OAuthFinish ¶ added in v1.0.3
func (x *OAuth) OAuthFinish(r *http.Request) (*OAuthCompletedResult, error)
This is the URL where the user gets redirected after completing a successful login to the OAuth provider. It is the OAuth provider's website that redirects the user back here. This is a GET request, and inside the URL, behind the fragment, are the login details. One major thing omitted from this function, is the creation of a session record, and returning a cookie to the browser. This is intentional, because it's very likely that you may want to do additional things, such as assigning some roles, before creating the session.
type OAuthCompletedResult ¶ added in v1.0.3
type PasswordEnforcement ¶
type PasswordEnforcement int
const ( PasswordEnforcementDefault PasswordEnforcement = 0 PasswordEnforcementReuse PasswordEnforcement = 1 << (iota - 1) )
type PermissionList ¶
type PermissionList []PermissionU16
A list of permissions
func PermitResolveToList ¶
func PermitResolveToList(permit []byte, db RoleGroupDB) (PermissionList, error)
This goes from Permit -> Groups -> PermList Permit has 0..n Groups Group has 0..n PermList We produce a list of all unique PermList that appear in any of the groups inside this permit. You can think of this as a binary OR operation.
func (*PermissionList) Add ¶
func (x *PermissionList) Add(perm PermissionU16)
Add adds this permission to the list. Takes no action if the permission is already present.
func (PermissionList) Has ¶
func (x PermissionList) Has(perm PermissionU16) bool
Has returns true if the list contains this permission
func (*PermissionList) Remove ¶
func (x *PermissionList) Remove(perm PermissionU16)
Remove removes this permission from the lst Takes no action if the permission is not present.
type PermissionNameTable ¶
type PermissionNameTable map[PermissionU16]string
A mapping from 16-bit permission number to a textual name of that permission
func (*PermissionNameTable) Inverted ¶
func (x *PermissionNameTable) Inverted() map[string]PermissionU16
Produces a map from permission name to permission number
type PermissionU16 ¶
type PermissionU16 uint16
Any permission in the system is uniquely described by a 16-bit unsigned integer
type Permit ¶
type Permit struct {
Roles []byte
}
A Permit is an opaque binary string that encodes domain-specific roles. This could be a string of bits with special meanings, or a blob of JSON, etc.
func (*Permit) Deserialize ¶
type PermitDB ¶
type PermitDB interface { GetPermit(userId UserId) (*Permit, error) // Retrieve a permit GetPermits() (map[UserId]*Permit, error) // Retrieve all permits as a map from identity to the permit. // This should create the permit if it does not exist. A call to this function is // followed by a call to SessionDB.PermitChanged. identity is canonicalized before being stored SetPermit(userId UserId, permit *Permit) error Close() // Typically used to close a database handle }
A Permit database performs no validation. It simply returns the Permit owned by a particular user. All operations except for Close must be thread-safe.
type RoleGroupCache ¶
type RoleGroupCache struct {
// contains filtered or unexported fields
}
Role Group cache
This caches all role groups from the backend database. We assume that this database will never be particularly large, so we simply allow our cache to grow indefinitely. All public functions are thread-safe.
func (*RoleGroupCache) Close ¶
func (x *RoleGroupCache) Close()
func (*RoleGroupCache) DeleteGroup ¶
func (x *RoleGroupCache) DeleteGroup(group *AuthGroup) (err error)
func (*RoleGroupCache) GetByID ¶
func (x *RoleGroupCache) GetByID(id GroupIDU32) (*AuthGroup, error)
func (*RoleGroupCache) GetByName ¶
func (x *RoleGroupCache) GetByName(name string) (*AuthGroup, error)
func (*RoleGroupCache) GetGroups ¶
func (x *RoleGroupCache) GetGroups() ([]*AuthGroup, error)
func (*RoleGroupCache) InsertGroup ¶
func (x *RoleGroupCache) InsertGroup(group *AuthGroup) (err error)
func (*RoleGroupCache) UpdateGroup ¶
func (x *RoleGroupCache) UpdateGroup(group *AuthGroup) (err error)
type RoleGroupDB ¶
type RoleGroupDB interface { GetGroups() ([]*AuthGroup, error) GetByName(name string) (*AuthGroup, error) GetByID(id GroupIDU32) (*AuthGroup, error) InsertGroup(group *AuthGroup) error DeleteGroup(group *AuthGroup) error UpdateGroup(group *AuthGroup) error Close() }
A Role Group database stores a list of Groups. Each Group has a list of permissions that it enables.
func NewCachedRoleGroupDB ¶
func NewCachedRoleGroupDB(backend RoleGroupDB) RoleGroupDB
Create a new RoleGroupDB that transparently caches reads of groups
func NewRoleGroupDB_SQL ¶
func NewRoleGroupDB_SQL(db *sql.DB) (RoleGroupDB, error)
type SessionDB ¶
type SessionDB interface { Write(sessionkey string, token *Token) error // Set a token Read(sessionkey string) (*Token, error) // Fetch a token Delete(sessionkey string) error // Delete a token (used to implement "logout") PermitChanged(userId UserId, permit *Permit) error // Assign the new permit to all of the sessions belonging to 'identity' InvalidateSessionsForIdentity(userId UserId) error // Delete all sessions belonging to the given identity. This is called after a password has been changed, or an identity renamed. Close() // Typically used to close a database handle }
A Session database is essentially a key/value store where the keys are session tokens, and the values are tuples of (Identity,Permit). All operations except for Close must be thread-safe.
type Token ¶
type Token struct { Identity string UserId UserId Email string Username string Expires time.Time Permit Permit }
Token is the result of a successful authentication request. It contains everything that we know about this authentication event, which includes the identity that performed the request, when this token expires, and the permit belonging to this identity.
func HttpHandlerBasicAuth ¶
func HttpHandlerPrelude ¶
HttpHandlerPrelude reads the session cookie or the HTTP "Basic" Authorization header to determine whether this request is authorized.
func HttpHandlerPreludeWithError ¶
func HttpHandlerPreludeWithError(config *ConfigHTTP, central *Central, w http.ResponseWriter, r *http.Request) (*Token, error)
Runs the Prelude function, but before returning an error, sends an appropriate error response to the HTTP ResponseWriter. If this function returns a non-nil error, then it means that you should not send anything else to the http response.
type UserId ¶
type UserId int64
const ( // The number of session records that are stored in the in-process cache DefaultSessionCacheSize = 10000 NullUserId UserId = 0 )
type UserStore ¶
type UserStore interface { Authenticate(identity, password string, authTypeCheck AuthCheck) error // Return nil error if the username and password are correct, otherwise one of ErrIdentityAuthNotFound or ErrInvalidPassword SetPassword(userId UserId, password string, enforceTypeCheck PasswordEnforcement) error // This sets the password to a user account SetConfig(passwordExpiry time.Duration) error ResetPasswordStart(userId UserId, expires time.Time) (string, error) // Create a one-time token that can be used to reset the password with a subsequent call to ResetPasswordFinish ResetPasswordFinish(userId UserId, token string, password string, enforceTypeCheck PasswordEnforcement) error // Check that token matches the last one generated by ResetPasswordStart, and if so, call SetPassword CreateIdentity(user *AuthUser, password string) (UserId, error) // Create a new identity. If the identity already exists, then this must return ErrIdentityExists. UpdateIdentity(user *AuthUser) error // Update an identity. Change email address or name etc. ArchiveIdentity(userId UserId) error // Archive an identity // TODO RenameIdentity was deprecated in May 2016, replaced by UpdateIdentity. We need to remove this once PCS team has made the necessary updates RenameIdentity(oldIdent, newIdent string) error // Rename an identity. Returns ErrIdentityAuthNotFound if oldIdent does not exist. Returns ErrIdentityExists if newIdent already exists. GetUserFromIdentity(identity string) (AuthUser, error) // Gets the user object from the identity supplied LockAccount(userId UserId) error // Locks an account UnlockAccount(userId UserId) error // Unlocks an account GetUserFromUserId(userId UserId) (AuthUser, error) // Gets the user object from the userId supplied GetIdentities(getIdentitiesFlag GetIdentitiesFlag) ([]AuthUser, error) // Retrieve a list of all identities Close() // Typically used to close a database handle }
The primary job of the UserStore, is to store and authenticate users. It is also responsible for adding new users, changing passwords etc. All operations except for Close must be thread-safe.