mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Bootstrap Go implementation of Authelia.
This is going to be the v4. Expected improvements: - More reliable due to static typing. - Bump of performance. - Improvement of logging. - Authelia can be shipped as a single binary. - Will likely work on ARM architecture.
This commit is contained in:
parent
325076a827
commit
828f565290
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -40,3 +40,8 @@ users_database.test.yml
|
|||
.suite
|
||||
.kube
|
||||
.idea
|
||||
|
||||
# Go binary
|
||||
authelia
|
||||
|
||||
.authelia-interrupt
|
||||
|
|
17
.npmignore
17
.npmignore
|
@ -1,17 +0,0 @@
|
|||
client/
|
||||
server/
|
||||
test/
|
||||
docs/
|
||||
scripts/
|
||||
images/
|
||||
example/
|
||||
|
||||
.travis.yml
|
||||
CONTRIBUTORS.md
|
||||
Dockerfile
|
||||
docker-compose.*
|
||||
Gruntfile.js
|
||||
tslint.json
|
||||
tsconfig.json
|
||||
|
||||
*.tgz
|
10
.travis.yml
10
.travis.yml
|
@ -1,7 +1,7 @@
|
|||
language: node_js
|
||||
language: go
|
||||
required: sudo
|
||||
node_js:
|
||||
- '9'
|
||||
go:
|
||||
- '1.13'
|
||||
services:
|
||||
- docker
|
||||
- ntp
|
||||
|
@ -12,9 +12,11 @@ addons:
|
|||
sources:
|
||||
- google-chrome
|
||||
packages:
|
||||
- xvfb
|
||||
- libgif-dev
|
||||
- google-chrome-stable
|
||||
before_script:
|
||||
- curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
|
||||
- nvm install v9 && nvm use v9 && npm i
|
||||
script:
|
||||
- "./scripts/authelia-scripts travis"
|
||||
after_success:
|
||||
|
|
12
BREAKING.md
12
BREAKING.md
|
@ -4,6 +4,18 @@ Breaking changes
|
|||
Since Authelia is still under active development, it is subject to breaking changes. We then recommend you don't blindly use the latest
|
||||
Docker image but pick a version instead and check this file before upgrading. This is where you will get information about breaking changes and about what you should do to overcome those changes.
|
||||
|
||||
## Breaking in v4.0.0
|
||||
|
||||
Authelia has been rewritten in Go for better performance and reliability.
|
||||
|
||||
### Model of U2F devices in MongoDB
|
||||
|
||||
The model of U2F devices stored in MongoDB has been updated to better fit with the Go library handling U2F keys.
|
||||
|
||||
### Removal of flag secure for SMTP notifier
|
||||
|
||||
The go library for sending e-mails automatically switch to TLS if possible according to https://golang.org/pkg/net/smtp/#SendMail.
|
||||
|
||||
## Breaking in v3.14.0
|
||||
|
||||
### Headers in nginx configuration
|
||||
|
|
19
Dockerfile
19
Dockerfile
|
@ -1,19 +1,20 @@
|
|||
FROM node:8.7.0-alpine
|
||||
FROM alpine:3.9.4
|
||||
|
||||
WORKDIR /usr/src
|
||||
WORKDIR /usr/app
|
||||
|
||||
COPY package.json /usr/src/package.json
|
||||
RUN apk --no-cache add ca-certificates wget
|
||||
|
||||
RUN apk --update add --no-cache --virtual \
|
||||
.build-deps make g++ python && \
|
||||
npm install --production && \
|
||||
apk del .build-deps
|
||||
# Install the libc required by the password hashing compiled with CGO.
|
||||
RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
|
||||
RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk
|
||||
RUN apk --no-cache add glibc-2.30-r0.apk
|
||||
|
||||
COPY dist/server /usr/src/server
|
||||
ADD dist/authelia authelia
|
||||
ADD dist/public_html public_html
|
||||
|
||||
EXPOSE 9091
|
||||
|
||||
VOLUME /etc/authelia
|
||||
VOLUME /var/lib/authelia
|
||||
|
||||
CMD ["node", "server/src/index.js", "/etc/authelia/config.yml"]
|
||||
CMD ["./authelia", "-config", "/etc/authelia/config.yml"]
|
||||
|
|
25
authentication/const.go
Normal file
25
authentication/const.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package authentication
|
||||
|
||||
// Level is the type representing a level of authentication
|
||||
type Level int
|
||||
|
||||
const (
|
||||
// NotAuthenticated if the user is not authenticated yet.
|
||||
NotAuthenticated Level = iota
|
||||
// OneFactor if the user has passed first factor only.
|
||||
OneFactor Level = iota
|
||||
// TwoFactor if the user has passed two factors.
|
||||
TwoFactor Level = iota
|
||||
)
|
||||
|
||||
const (
|
||||
// TOTP Method using Time-Based One-Time Password applications like Google Authenticator
|
||||
TOTP = "totp"
|
||||
// U2F Method using U2F devices like Yubikeys
|
||||
U2F = "u2f"
|
||||
// DuoPush Method using Duo application to receive push notifications.
|
||||
DuoPush = "duo_push"
|
||||
)
|
||||
|
||||
// PossibleMethods is the set of all possible 2FA methods.
|
||||
var PossibleMethods = []string{TOTP, U2F, DuoPush}
|
113
authentication/file_user_provider.go
Normal file
113
authentication/file_user_provider.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// FileUserProvider is a provider reading details from a file.
|
||||
type FileUserProvider struct {
|
||||
path *string
|
||||
database *DatabaseModel
|
||||
lock *sync.Mutex
|
||||
}
|
||||
|
||||
// UserDetailsModel is the model of user details in the file database.
|
||||
type UserDetailsModel struct {
|
||||
HashedPassword string `yaml:"password" valid:"required"`
|
||||
Email string `yaml:"email"`
|
||||
Groups []string `yaml:"groups"`
|
||||
}
|
||||
|
||||
// DatabaseModel is the model of users file database.
|
||||
type DatabaseModel struct {
|
||||
Users map[string]UserDetailsModel `yaml:"users" valid:"required"`
|
||||
}
|
||||
|
||||
// NewFileUserProvider creates a new instance of FileUserProvider.
|
||||
func NewFileUserProvider(filepath string) *FileUserProvider {
|
||||
database, err := readDatabase(filepath)
|
||||
if err != nil {
|
||||
// Panic since the file does not exist when Authelia is starting.
|
||||
panic(err)
|
||||
}
|
||||
return &FileUserProvider{
|
||||
path: &filepath,
|
||||
database: database,
|
||||
lock: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func readDatabase(path string) (*DatabaseModel, error) {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db := DatabaseModel{}
|
||||
err = yaml.Unmarshal(content, &db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ok, err := govalidator.ValidateStruct(db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("The database format is invalid: %s", err.Error())
|
||||
}
|
||||
return &db, nil
|
||||
}
|
||||
|
||||
// CheckUserPassword checks if provided password matches for the given user.
|
||||
func (p *FileUserProvider) CheckUserPassword(username string, password string) (bool, error) {
|
||||
if details, ok := p.database.Users[username]; ok {
|
||||
hashedPassword := details.HashedPassword[7:] // Remove {CRYPT}
|
||||
ok, err := CheckPassword(password, hashedPassword)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
return false, fmt.Errorf("User '%s' does not exist in database", username)
|
||||
}
|
||||
|
||||
// GetDetails retrieve the groups a user belongs to.
|
||||
func (p *FileUserProvider) GetDetails(username string) (*UserDetails, error) {
|
||||
if details, ok := p.database.Users[username]; ok {
|
||||
return &UserDetails{
|
||||
Groups: details.Groups,
|
||||
Emails: []string{details.Email},
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("User '%s' does not exist in database", username)
|
||||
}
|
||||
|
||||
// UpdatePassword update the password of the given user.
|
||||
func (p *FileUserProvider) UpdatePassword(username string, newPassword string) error {
|
||||
details, ok := p.database.Users[username]
|
||||
if !ok {
|
||||
return fmt.Errorf("User '%s' does not exist in database", username)
|
||||
}
|
||||
|
||||
hash := HashPassword(newPassword, nil)
|
||||
details.HashedPassword = fmt.Sprintf("{CRYPT}%s", hash)
|
||||
|
||||
p.lock.Lock()
|
||||
p.database.Users[username] = details
|
||||
|
||||
b, err := yaml.Marshal(p.database)
|
||||
if err != nil {
|
||||
p.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(*p.path, b, 0644)
|
||||
p.lock.Unlock()
|
||||
return err
|
||||
}
|
144
authentication/file_user_provider_test.go
Normal file
144
authentication/file_user_provider_test.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func WithDatabase(content []byte, f func(path string)) {
|
||||
tmpfile, err := ioutil.TempFile("", "users_database.*.yaml")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
defer os.Remove(tmpfile.Name()) // clean up
|
||||
|
||||
if _, err := tmpfile.Write(content); err != nil {
|
||||
tmpfile.Close()
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f(tmpfile.Name())
|
||||
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldCheckUserPasswordIsCorrect(t *testing.T) {
|
||||
WithDatabase(UserDatabaseContent, func(path string) {
|
||||
provider := NewFileUserProvider(path)
|
||||
ok, err := provider.CheckUserPassword("john", "password")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldCheckUserPasswordIsWrong(t *testing.T) {
|
||||
WithDatabase(UserDatabaseContent, func(path string) {
|
||||
provider := NewFileUserProvider(path)
|
||||
ok, err := provider.CheckUserPassword("john", "wrong_password")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldCheckUserPasswordOfUnexistingUser(t *testing.T) {
|
||||
WithDatabase(UserDatabaseContent, func(path string) {
|
||||
provider := NewFileUserProvider(path)
|
||||
_, err := provider.CheckUserPassword("fake", "password")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "User 'fake' does not exist in database", err.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldRetrieveUserDetails(t *testing.T) {
|
||||
WithDatabase(UserDatabaseContent, func(path string) {
|
||||
provider := NewFileUserProvider(path)
|
||||
details, err := provider.GetDetails("john")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, details.Emails, []string{"john.doe@authelia.com"})
|
||||
assert.Equal(t, details.Groups, []string{"admins", "dev"})
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldUpdatePassword(t *testing.T) {
|
||||
WithDatabase(UserDatabaseContent, func(path string) {
|
||||
provider := NewFileUserProvider(path)
|
||||
err := provider.UpdatePassword("john", "newpassword")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Reset the provider to force a read from disk.
|
||||
provider = NewFileUserProvider(path)
|
||||
ok, err := provider.CheckUserPassword("john", "newpassword")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldRaiseWhenLoadingMalformedDatabaseForFirstTime(t *testing.T) {
|
||||
WithDatabase(MalformedUserDatabaseContent, func(path string) {
|
||||
assert.Panics(t, func() {
|
||||
NewFileUserProvider(path)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestShouldRaiseWhenLoadingDatabaseWithBadSchemaForFirstTime(t *testing.T) {
|
||||
WithDatabase(BadSchemaUserDatabaseContent, func(path string) {
|
||||
assert.Panics(t, func() {
|
||||
NewFileUserProvider(path)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var UserDatabaseContent = []byte(`
|
||||
users:
|
||||
john:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
|
||||
harry:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: harry.potter@authelia.com
|
||||
groups: []
|
||||
|
||||
bob:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: bob.dylan@authelia.com
|
||||
groups:
|
||||
- dev
|
||||
|
||||
james:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: james.dean@authelia.com
|
||||
`)
|
||||
|
||||
var MalformedUserDatabaseContent = []byte(`
|
||||
users
|
||||
john
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admin
|
||||
- dev
|
||||
`)
|
||||
|
||||
// The YAML is valid but the root key is user instead of users
|
||||
var BadSchemaUserDatabaseContent = []byte(`
|
||||
user:
|
||||
john:
|
||||
password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"
|
||||
email: john.doe@authelia.com
|
||||
groups:
|
||||
- admins
|
||||
- dev
|
||||
`)
|
225
authentication/ldap_user_provider.go
Normal file
225
authentication/ldap_user_provider.go
Normal file
|
@ -0,0 +1,225 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
"gopkg.in/ldap.v3"
|
||||
)
|
||||
|
||||
// LDAPUserProvider is a provider using a LDAP or AD as a user database.
|
||||
type LDAPUserProvider struct {
|
||||
configuration schema.LDAPAuthenticationBackendConfiguration
|
||||
}
|
||||
|
||||
func (p *LDAPUserProvider) connect(userDN string, password string) (*ldap.Conn, error) {
|
||||
conn, err := ldap.Dial("tcp", p.configuration.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conn.Bind(userDN, password)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// NewLDAPUserProvider creates a new instance of LDAPUserProvider.
|
||||
func NewLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfiguration) *LDAPUserProvider {
|
||||
return &LDAPUserProvider{configuration}
|
||||
}
|
||||
|
||||
// CheckUserPassword checks if provided password matches for the given user.
|
||||
func (p *LDAPUserProvider) CheckUserPassword(username string, password string) (bool, error) {
|
||||
adminClient, err := p.connect(p.configuration.User, p.configuration.Password)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer adminClient.Close()
|
||||
|
||||
userDN, err := p.getUserDN(adminClient, username)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
conn, err := p.connect(userDN, password)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Authentication of user %s failed. Cause: %s", username, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (p *LDAPUserProvider) getUserAttribute(conn *ldap.Conn, username string, attribute string) ([]string, error) {
|
||||
client, err := p.connect(p.configuration.User, p.configuration.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
userFilter := strings.Replace(p.configuration.UsersFilter, "{0}", username, -1)
|
||||
baseDN := p.configuration.AdditionalUsersDN + "," + p.configuration.BaseDN
|
||||
|
||||
// Search for the given username
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
||||
1, 0, false, userFilter, []string{attribute}, nil,
|
||||
)
|
||||
|
||||
sr, err := client.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot find user DN of user %s. Cause: %s", username, err)
|
||||
}
|
||||
|
||||
if len(sr.Entries) != 1 {
|
||||
return nil, fmt.Errorf("No %s found for user %s", attribute, username)
|
||||
}
|
||||
|
||||
if attribute == "dn" {
|
||||
return []string{sr.Entries[0].DN}, nil
|
||||
}
|
||||
|
||||
return sr.Entries[0].Attributes[0].Values, nil
|
||||
}
|
||||
|
||||
func (p *LDAPUserProvider) getUserDN(conn *ldap.Conn, username string) (string, error) {
|
||||
values, err := p.getUserAttribute(conn, username, "dn")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(values) != 1 {
|
||||
return "", fmt.Errorf("DN attribute of user %s must be set", username)
|
||||
}
|
||||
|
||||
return values[0], nil
|
||||
}
|
||||
|
||||
func (p *LDAPUserProvider) getUserUID(conn *ldap.Conn, username string) (string, error) {
|
||||
values, err := p.getUserAttribute(conn, username, "uid")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(values) != 1 {
|
||||
return "", fmt.Errorf("UID attribute of user %s must be set", username)
|
||||
}
|
||||
|
||||
return values[0], nil
|
||||
}
|
||||
|
||||
func (p *LDAPUserProvider) createGroupsFilter(conn *ldap.Conn, username string) (string, error) {
|
||||
if strings.Index(p.configuration.GroupsFilter, "{0}") >= 0 {
|
||||
return strings.Replace(p.configuration.GroupsFilter, "{0}", username, -1), nil
|
||||
} else if strings.Index(p.configuration.GroupsFilter, "{dn}") >= 0 {
|
||||
userDN, err := p.getUserDN(conn, username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Replace(p.configuration.GroupsFilter, "{dn}", userDN, -1), nil
|
||||
} else if strings.Index(p.configuration.GroupsFilter, "{uid}") >= 0 {
|
||||
userUID, err := p.getUserUID(conn, username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.Replace(p.configuration.GroupsFilter, "{uid}", userUID, -1), nil
|
||||
}
|
||||
return p.configuration.GroupsFilter, nil
|
||||
}
|
||||
|
||||
// GetDetails retrieve the groups a user belongs to.
|
||||
func (p *LDAPUserProvider) GetDetails(username string) (*UserDetails, error) {
|
||||
conn, err := p.connect(p.configuration.User, p.configuration.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
groupsFilter, err := p.createGroupsFilter(conn, username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to create group filter for user %s. Cause: %s", username, err)
|
||||
}
|
||||
|
||||
groupBaseDN := fmt.Sprintf("%s,%s", p.configuration.AdditionalGroupsDN, p.configuration.BaseDN)
|
||||
|
||||
// Search for the given username
|
||||
searchGroupRequest := ldap.NewSearchRequest(
|
||||
groupBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
||||
0, 0, false, groupsFilter, []string{p.configuration.GroupNameAttribute}, nil,
|
||||
)
|
||||
|
||||
sr, err := conn.Search(searchGroupRequest)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to retrieve groups of user %s. Cause: %s", username, err)
|
||||
}
|
||||
|
||||
groups := make([]string, 0)
|
||||
|
||||
for _, res := range sr.Entries {
|
||||
// append all values of the document. Normally there should be only one per document.
|
||||
groups = append(groups, res.Attributes[0].Values...)
|
||||
}
|
||||
|
||||
userDN, err := p.getUserDN(conn, username)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
searchEmailRequest := ldap.NewSearchRequest(
|
||||
userDN, ldap.ScopeBaseObject, ldap.NeverDerefAliases,
|
||||
0, 0, false, "(cn=*)", []string{p.configuration.MailAttribute}, nil,
|
||||
)
|
||||
|
||||
sr, err = conn.Search(searchEmailRequest)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to retrieve email of user %s. Cause: %s", username, err)
|
||||
}
|
||||
|
||||
emails := make([]string, 0)
|
||||
|
||||
for _, res := range sr.Entries {
|
||||
// append all values of the document. Normally there should be only one per document.
|
||||
emails = append(emails, res.Attributes[0].Values...)
|
||||
}
|
||||
|
||||
return &UserDetails{
|
||||
Emails: emails,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdatePassword update the password of the given user.
|
||||
func (p *LDAPUserProvider) UpdatePassword(username string, newPassword string) error {
|
||||
client, err := p.connect(p.configuration.User, p.configuration.Password)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to update password. Cause: %s", err)
|
||||
}
|
||||
|
||||
userDN, err := p.getUserDN(client, username)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to update password. Cause: %s", err)
|
||||
}
|
||||
|
||||
modifyRequest := ldap.NewModifyRequest(userDN, nil)
|
||||
|
||||
modifyRequest.Replace("userPassword", []string{newPassword})
|
||||
|
||||
err = client.Modify(modifyRequest)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Unable to update password. Cause: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
101
authentication/password_hash.go
Normal file
101
authentication/password_hash.go
Normal file
|
@ -0,0 +1,101 @@
|
|||
package authentication
|
||||
|
||||
// #cgo LDFLAGS: -lcrypt
|
||||
// #define _GNU_SOURCE
|
||||
// #include <crypt.h>
|
||||
// #include <stdlib.h>
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Crypt wraps C library crypt_r
|
||||
func crypt(key string, salt string) string {
|
||||
data := C.struct_crypt_data{}
|
||||
ckey := C.CString(key)
|
||||
csalt := C.CString(salt)
|
||||
out := C.GoString(C.crypt_r(ckey, csalt, &data))
|
||||
C.free(unsafe.Pointer(ckey))
|
||||
C.free(unsafe.Pointer(csalt))
|
||||
return out
|
||||
}
|
||||
|
||||
// PasswordHash represents all characteristics of a password hash.
|
||||
// Authelia only supports salted SHA512 method, i.e., $6$ mode.
|
||||
type PasswordHash struct {
|
||||
// The number of rounds.
|
||||
Rounds int
|
||||
// The salt with a max size of 16 characters for SHA512.
|
||||
Salt string
|
||||
// The password hash.
|
||||
Hash string
|
||||
}
|
||||
|
||||
// passwordHashFromString extracts all characteristics of a hash given its string representation.
|
||||
func passwordHashFromString(hash string) (*PasswordHash, error) {
|
||||
// Only supports salted sha 512.
|
||||
if hash[:3] != "$6$" {
|
||||
return nil, errors.New("Authelia only supports salted SHA512 hashing")
|
||||
}
|
||||
parts := strings.Split(hash, "$")
|
||||
|
||||
if len(parts) != 5 {
|
||||
return nil, errors.New("Cannot parse the hash")
|
||||
}
|
||||
|
||||
roundsKV := strings.Split(parts[2], "=")
|
||||
if len(roundsKV) != 2 {
|
||||
return nil, errors.New("Cannot find the number of rounds")
|
||||
}
|
||||
|
||||
rounds, err := strconv.ParseInt(roundsKV[1], 10, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot find the number of rounds in the hash: %s", err.Error())
|
||||
}
|
||||
|
||||
return &PasswordHash{
|
||||
Rounds: int(rounds),
|
||||
Salt: parts[3],
|
||||
Hash: parts[4],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// The set of letters RandomString can pick in.
|
||||
var possibleLetters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
|
||||
// RandomString generate a random string of n characters.
|
||||
func RandomString(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = possibleLetters[rand.Intn(len(possibleLetters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// HashPassword generate a salt and hash the password with the salt and a constant
|
||||
// number of rounds.
|
||||
func HashPassword(password string, salt *string) string {
|
||||
var generatedSalt string
|
||||
if salt == nil {
|
||||
generatedSalt = fmt.Sprintf("$6$rounds=5000$%s$", RandomString(16))
|
||||
} else {
|
||||
generatedSalt = *salt
|
||||
}
|
||||
return crypt(password, generatedSalt)
|
||||
}
|
||||
|
||||
// CheckPassword check a password against a hash.
|
||||
func CheckPassword(password string, hash string) (bool, error) {
|
||||
passwordHash, err := passwordHashFromString(hash)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
salt := fmt.Sprintf("$6$rounds=%d$%s$", passwordHash.Rounds, passwordHash.Salt)
|
||||
pHash := HashPassword(password, &salt)
|
||||
return pHash == hash, nil
|
||||
}
|
20
authentication/password_hash_test.go
Normal file
20
authentication/password_hash_test.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package authentication
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShouldHashPassword(t *testing.T) {
|
||||
salt := "$6$rounds=5000$aFr56HjK3DrB8t3S$"
|
||||
hash := HashPassword("password", &salt)
|
||||
assert.Equal(t, "$6$rounds=5000$aFr56HjK3DrB8t3S$3yTiN5991WnlmhE8qlMmayIiUiT5ppq68CIuHBrGgQHJ4RWSCb0AykB0E6Ij761ZTzLaCZKuXpurcBiqDR1hu.", hash)
|
||||
}
|
||||
|
||||
func TestShouldCheckPassword(t *testing.T) {
|
||||
ok, err := CheckPassword("password", "$6$rounds=5000$aFr56HjK3DrB8t3S$3yTiN5991WnlmhE8qlMmayIiUiT5ppq68CIuHBrGgQHJ4RWSCb0AykB0E6Ij761ZTzLaCZKuXpurcBiqDR1hu.")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
}
|
7
authentication/types.go
Normal file
7
authentication/types.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package authentication
|
||||
|
||||
// UserDetails represent the details retrieved for a given user.
|
||||
type UserDetails struct {
|
||||
Emails []string
|
||||
Groups []string
|
||||
}
|
9
authentication/user_provider.go
Normal file
9
authentication/user_provider.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package authentication
|
||||
|
||||
// UserProvider is the interface for checking user password and
|
||||
// gathering user details.
|
||||
type UserProvider interface {
|
||||
CheckUserPassword(username string, password string) (bool, error)
|
||||
GetDetails(username string) (*UserDetails, error)
|
||||
UpdatePassword(username string, newPassword string) error
|
||||
}
|
189
authorization/authorizer.go
Normal file
189
authorization/authorizer.go
Normal file
|
@ -0,0 +1,189 @@
|
|||
package authorization
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
)
|
||||
|
||||
const userPrefix = "user:"
|
||||
const groupPrefix = "group:"
|
||||
|
||||
// Authorizer the component in charge of checking whether a user can access a given resource.
|
||||
type Authorizer struct {
|
||||
configuration schema.AccessControlConfiguration
|
||||
}
|
||||
|
||||
// NewAuthorizer create an instance of authorizer with a given access control configuration.
|
||||
func NewAuthorizer(configuration schema.AccessControlConfiguration) *Authorizer {
|
||||
return &Authorizer{
|
||||
configuration: configuration,
|
||||
}
|
||||
}
|
||||
|
||||
// Subject subject who to check access control for.
|
||||
type Subject struct {
|
||||
Username string
|
||||
Groups []string
|
||||
IP net.IP
|
||||
}
|
||||
|
||||
// Object object to check access control for
|
||||
type Object struct {
|
||||
Domain string
|
||||
Path string
|
||||
}
|
||||
|
||||
func isDomainMatching(domain string, domainRule string) bool {
|
||||
if domain == domainRule { // if domain matches exactly
|
||||
return true
|
||||
} else if strings.HasPrefix(domainRule, "*") && strings.HasSuffix(domain, domainRule[1:]) {
|
||||
// If domain pattern starts with *, it's a multi domain pattern.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isPathMatching(path string, pathRegexps []string) bool {
|
||||
// If there is no regexp patterns, it means that we match any path.
|
||||
if len(pathRegexps) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, pathRegexp := range pathRegexps {
|
||||
match, err := regexp.MatchString(pathRegexp, path)
|
||||
if err != nil {
|
||||
// TODO(c.michaud): make sure this is safe in advance to
|
||||
// avoid checking this case here.
|
||||
continue
|
||||
}
|
||||
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isSubjectMatching(subject Subject, subjectRule string) bool {
|
||||
// If no subject is provided in the rule, we match any user.
|
||||
if subjectRule == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(subjectRule, userPrefix) {
|
||||
user := strings.Trim(subjectRule[len(userPrefix):], " ")
|
||||
if user == subject.Username {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(subjectRule, groupPrefix) {
|
||||
group := strings.Trim(subjectRule[len(groupPrefix):], " ")
|
||||
if isStringInSlice(group, subject.Groups) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isIPMatching check whether user's IP is in one of the network ranges.
|
||||
func isIPMatching(ip net.IP, networks []string) bool {
|
||||
// If no network is provided in the rule, we match any network
|
||||
if len(networks) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, network := range networks {
|
||||
if !strings.Contains(network, "/") {
|
||||
if ip.String() == network {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, ipNet, err := net.ParseCIDR(network)
|
||||
if err != nil {
|
||||
// TODO(c.michaud): make sure the rule is valid at startup to
|
||||
// to such a case here.
|
||||
continue
|
||||
}
|
||||
|
||||
if ipNet.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isStringInSlice(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// selectMatchingSubjectRules take a set of rules and select only the rules matching the subject constraints.
|
||||
func selectMatchingSubjectRules(rules []schema.ACLRule, subject Subject) []schema.ACLRule {
|
||||
selectedRules := []schema.ACLRule{}
|
||||
|
||||
for _, rule := range rules {
|
||||
if isSubjectMatching(subject, rule.Subject) &&
|
||||
isIPMatching(subject.IP, rule.Networks) {
|
||||
|
||||
selectedRules = append(selectedRules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
return selectedRules
|
||||
}
|
||||
|
||||
func selectMatchingObjectRules(rules []schema.ACLRule, object Object) []schema.ACLRule {
|
||||
selectedRules := []schema.ACLRule{}
|
||||
|
||||
for _, rule := range rules {
|
||||
if isDomainMatching(object.Domain, rule.Domain) &&
|
||||
isPathMatching(object.Path, rule.Resources) {
|
||||
|
||||
selectedRules = append(selectedRules, rule)
|
||||
}
|
||||
}
|
||||
return selectedRules
|
||||
}
|
||||
|
||||
func selectMatchingRules(rules []schema.ACLRule, subject Subject, object Object) []schema.ACLRule {
|
||||
matchingRules := selectMatchingSubjectRules(rules, subject)
|
||||
return selectMatchingObjectRules(matchingRules, object)
|
||||
}
|
||||
|
||||
func policyToLevel(policy string) Level {
|
||||
switch policy {
|
||||
case "bypass":
|
||||
return Bypass
|
||||
case "one_factor":
|
||||
return OneFactor
|
||||
case "two_factor":
|
||||
return TwoFactor
|
||||
case "deny":
|
||||
return Denied
|
||||
}
|
||||
// By default the deny policy applies.
|
||||
return Denied
|
||||
}
|
||||
|
||||
// GetRequiredLevel retrieve the required level of authorization to access the object.
|
||||
func (p *Authorizer) GetRequiredLevel(subject Subject, requestURL url.URL) Level {
|
||||
matchingRules := selectMatchingRules(p.configuration.Rules, subject, Object{
|
||||
Domain: requestURL.Hostname(),
|
||||
Path: requestURL.Path,
|
||||
})
|
||||
|
||||
if len(matchingRules) > 0 {
|
||||
return policyToLevel(matchingRules[0].Policy)
|
||||
}
|
||||
return policyToLevel(p.configuration.DefaultPolicy)
|
||||
}
|
261
authorization/authorizer_test.go
Normal file
261
authorization/authorizer_test.go
Normal file
|
@ -0,0 +1,261 @@
|
|||
package authorization
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var NoNet = []string{}
|
||||
var LocalNet = []string{"127.0.0.1"}
|
||||
var PrivateNet = []string{"192.168.1.0/24"}
|
||||
var MultipleNet = []string{"192.168.1.0/24", "10.0.0.0/8"}
|
||||
var MixedNetIP = []string{"192.168.1.0/24", "192.168.2.4"}
|
||||
|
||||
type AuthorizerSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
type AuthorizerTester struct {
|
||||
*Authorizer
|
||||
}
|
||||
|
||||
func NewAuthorizerTester(config schema.AccessControlConfiguration) *AuthorizerTester {
|
||||
return &AuthorizerTester{
|
||||
NewAuthorizer(config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthorizerTester) CheckAuthorizations(t *testing.T, subject Subject, requestURI string, expectedLevel Level) {
|
||||
url, _ := url.ParseRequestURI(requestURI)
|
||||
level := s.GetRequiredLevel(Subject{
|
||||
Groups: subject.Groups,
|
||||
Username: subject.Username,
|
||||
IP: subject.IP,
|
||||
}, *url)
|
||||
|
||||
assert.Equal(t, expectedLevel, level)
|
||||
}
|
||||
|
||||
type AuthorizerTesterBuilder struct {
|
||||
config schema.AccessControlConfiguration
|
||||
}
|
||||
|
||||
func NewAuthorizerBuilder() *AuthorizerTesterBuilder {
|
||||
return &AuthorizerTesterBuilder{}
|
||||
}
|
||||
|
||||
func (b *AuthorizerTesterBuilder) WithDefaultPolicy(policy string) *AuthorizerTesterBuilder {
|
||||
b.config.DefaultPolicy = policy
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *AuthorizerTesterBuilder) WithRule(rule schema.ACLRule) *AuthorizerTesterBuilder {
|
||||
b.config.Rules = append(b.config.Rules, rule)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *AuthorizerTesterBuilder) Build() *AuthorizerTester {
|
||||
return NewAuthorizerTester(b.config)
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
subject Subject
|
||||
object Object
|
||||
}
|
||||
|
||||
var AnonymousUser = Subject{
|
||||
Username: "",
|
||||
Groups: []string{},
|
||||
IP: net.ParseIP("127.0.0.1"),
|
||||
}
|
||||
|
||||
var UserWithGroups = Subject{
|
||||
Username: "john",
|
||||
Groups: []string{"dev", "admins"},
|
||||
IP: net.ParseIP("10.0.0.8"),
|
||||
}
|
||||
|
||||
var John = UserWithGroups
|
||||
|
||||
var UserWithoutGroups = Subject{
|
||||
Username: "bob",
|
||||
Groups: []string{},
|
||||
IP: net.ParseIP("10.0.0.7"),
|
||||
}
|
||||
|
||||
var Bob = UserWithoutGroups
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckDefaultBypassConfig() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy("bypass").Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://public.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://public.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), UserWithoutGroups, "https://public.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), UserWithoutGroups, "https://public.example.com/elsewhere", Bypass)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckDefaultDeniedConfig() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy("deny").Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://public.example.com/", Denied)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://public.example.com/", Denied)
|
||||
tester.CheckAuthorizations(s.T(), UserWithoutGroups, "https://public.example.com/", Denied)
|
||||
tester.CheckAuthorizations(s.T(), UserWithoutGroups, "https://public.example.com/elsewhere", Denied)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckMultiDomainRule() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy("deny").
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "*.example.com",
|
||||
Policy: "bypass",
|
||||
}).
|
||||
Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://public.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://private.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://public.example.com/elsewhere", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://example.com/", Denied)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://public.example.com.c/", Denied)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://public.example.co/", Denied)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckFactorsPolicy() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy("deny").
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "single.example.com",
|
||||
Policy: "one_factor",
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "protected.example.com",
|
||||
Policy: "two_factor",
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "public.example.com",
|
||||
Policy: "bypass",
|
||||
}).
|
||||
Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://public.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://protected.example.com/", TwoFactor)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://single.example.com/", OneFactor)
|
||||
tester.CheckAuthorizations(s.T(), UserWithGroups, "https://example.com/", Denied)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckRulePrecedence() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy("deny").
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "protected.example.com",
|
||||
Policy: "bypass",
|
||||
Subject: "user:john",
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "protected.example.com",
|
||||
Policy: "one_factor",
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "*.example.com",
|
||||
Policy: "two_factor",
|
||||
}).
|
||||
Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://protected.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://protected.example.com/", OneFactor)
|
||||
tester.CheckAuthorizations(s.T(), John, "https://public.example.com/", TwoFactor)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckUserMatching() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy("deny").
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "protected.example.com",
|
||||
Policy: "bypass",
|
||||
Subject: "user:john",
|
||||
}).
|
||||
Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://protected.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://protected.example.com/", Denied)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckGroupMatching() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy("deny").
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "protected.example.com",
|
||||
Policy: "bypass",
|
||||
Subject: "group:admins",
|
||||
}).
|
||||
Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://protected.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://protected.example.com/", Denied)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckIPMatching() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy("deny").
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "protected.example.com",
|
||||
Policy: "bypass",
|
||||
Networks: []string{"192.168.1.8", "10.0.0.8"},
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "protected.example.com",
|
||||
Policy: "one_factor",
|
||||
Networks: []string{"10.0.0.7"},
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "net.example.com",
|
||||
Policy: "two_factor",
|
||||
Networks: []string{"10.0.0.0/8"},
|
||||
}).
|
||||
Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://protected.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://protected.example.com/", OneFactor)
|
||||
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://protected.example.com/", Denied)
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://net.example.com/", TwoFactor)
|
||||
tester.CheckAuthorizations(s.T(), Bob, "https://net.example.com/", TwoFactor)
|
||||
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://net.example.com/", Denied)
|
||||
}
|
||||
|
||||
func (s *AuthorizerSuite) TestShouldCheckResourceMatching() {
|
||||
tester := NewAuthorizerBuilder().
|
||||
WithDefaultPolicy("deny").
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "resource.example.com",
|
||||
Policy: "bypass",
|
||||
Resources: []string{"^/bypass/[a-z]+$", "^/$", "embedded"},
|
||||
}).
|
||||
WithRule(schema.ACLRule{
|
||||
Domain: "resource.example.com",
|
||||
Policy: "one_factor",
|
||||
Resources: []string{"^/one_factor/[a-z]+$"},
|
||||
}).
|
||||
Build()
|
||||
|
||||
tester.CheckAuthorizations(s.T(), John, "https://resource.example.com/", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), John, "https://resource.example.com/bypass/abc", Bypass)
|
||||
tester.CheckAuthorizations(s.T(), John, "https://resource.example.com/bypass/", Denied)
|
||||
tester.CheckAuthorizations(s.T(), John, "https://resource.example.com/bypass/ABC", Denied)
|
||||
tester.CheckAuthorizations(s.T(), John, "https://resource.example.com/one_factor/abc", OneFactor)
|
||||
tester.CheckAuthorizations(s.T(), John, "https://resource.example.com/xyz/embedded/abc", Bypass)
|
||||
}
|
||||
|
||||
func TestRunSuite(t *testing.T) {
|
||||
s := AuthorizerSuite{}
|
||||
suite.Run(t, &s)
|
||||
}
|
15
authorization/const.go
Normal file
15
authorization/const.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package authorization
|
||||
|
||||
// Level is the type representing an authorization level.
|
||||
type Level int
|
||||
|
||||
const (
|
||||
// Bypass bypass level.
|
||||
Bypass Level = iota
|
||||
// OneFactor one factor level.
|
||||
OneFactor Level = iota
|
||||
// TwoFactor two factor level.
|
||||
TwoFactor Level = iota
|
||||
// Denied denied level.
|
||||
Denied Level = iota
|
||||
)
|
|
@ -3,7 +3,6 @@ export PATH=$(pwd)/scripts:/tmp:$PATH
|
|||
|
||||
export PS1="(authelia) $PS1"
|
||||
|
||||
echo "[BOOTSTRAP] Installing npm packages..."
|
||||
npm i
|
||||
|
||||
pushd client
|
||||
|
@ -27,5 +26,8 @@ fi
|
|||
echo "[BOOTSTRAP] Running additional bootstrap steps..."
|
||||
authelia-scripts bootstrap
|
||||
|
||||
# Create temporary directory that will contain the databases used in tests.
|
||||
mkdir -p /tmp/authelia
|
||||
|
||||
echo "[BOOTSTRAP] Run 'authelia-scripts suites start dockerhub' to start Authelia and visit https://home.example.com:8080."
|
||||
echo "[BOOTSTRAP] More details at https://github.com/clems4ever/authelia/blob/master/docs/getting-started.md"
|
||||
|
|
7538
client/package-lock.json
generated
7538
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -17,13 +17,12 @@
|
|||
"@types/react-redux": "^6.0.12",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
"@types/redux-thunk": "^2.1.0",
|
||||
"await-to-js": "^2.1.1",
|
||||
"classnames": "^2.2.6",
|
||||
"connected-react-router": "^6.2.1",
|
||||
"node-sass": "^4.11.0",
|
||||
"qrcode.react": "^0.9.2",
|
||||
"query-string": "^6.2.0",
|
||||
"react": "^16.6.0",
|
||||
"react": "^16.10.2",
|
||||
"react-dom": "^16.6.0",
|
||||
"react-redux": "^6.0.0",
|
||||
"react-router-dom": "^4.3.1",
|
||||
|
|
|
@ -6,6 +6,7 @@ export default async function(dispatch: Dispatch) {
|
|||
dispatch(getPreferedMethod());
|
||||
try {
|
||||
const method = await AutheliaService.fetchPrefered2faMethod();
|
||||
console.log(method);
|
||||
dispatch(getPreferedMethodSuccess(method));
|
||||
} catch (err) {
|
||||
dispatch(getPreferedMethodFailure(err.message))
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
import { Dispatch } from "redux";
|
||||
import { fetchStateFailure, fetchStateSuccess } from "../reducers/Portal/Authentication/actions";
|
||||
import to from "await-to-js";
|
||||
import AutheliaService from "../services/AutheliaService";
|
||||
|
||||
export default async function(dispatch: Dispatch) {
|
||||
let err, res;
|
||||
[err, res] = await to(AutheliaService.fetchState());
|
||||
if (err) {
|
||||
await dispatch(fetchStateFailure(err.message));
|
||||
return;
|
||||
try {
|
||||
const state = await AutheliaService.fetchState();
|
||||
dispatch(fetchStateSuccess(state));
|
||||
} catch (err) {
|
||||
dispatch(fetchStateFailure(err.message));
|
||||
}
|
||||
if (!res) {
|
||||
await dispatch(fetchStateFailure('No response'));
|
||||
return
|
||||
}
|
||||
await dispatch(fetchStateSuccess(res));
|
||||
return res;
|
||||
}
|
|
@ -1,18 +1,15 @@
|
|||
import { Dispatch } from "redux";
|
||||
import { logout, logoutFailure, logoutSuccess } from "../reducers/Portal/SecondFactor/actions";
|
||||
import to from "await-to-js";
|
||||
import fetchState from "./FetchStateBehavior";
|
||||
import AutheliaService from "../services/AutheliaService";
|
||||
|
||||
export default async function(dispatch: Dispatch) {
|
||||
await dispatch(logout());
|
||||
let err, res;
|
||||
[err, res] = await to(AutheliaService.postLogout());
|
||||
|
||||
if (err) {
|
||||
await dispatch(logoutFailure(err.message));
|
||||
return;
|
||||
try {
|
||||
dispatch(logout());
|
||||
await AutheliaService.postLogout();
|
||||
dispatch(logoutSuccess());
|
||||
await fetchState(dispatch);
|
||||
} catch (err) {
|
||||
dispatch(logoutFailure(err.message));
|
||||
}
|
||||
await dispatch(logoutSuccess());
|
||||
await fetchState(dispatch);
|
||||
}
|
|
@ -17,17 +17,19 @@ export interface OwnProps {
|
|||
export interface StateProps {
|
||||
formDisabled: boolean;
|
||||
error: string | null;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface DispatchProps {
|
||||
onAuthenticationRequested(username: string, password: string, rememberMe: boolean): Promise<void>;
|
||||
onUsernameChanged(username: string): void;
|
||||
onPasswordChanged(password: string): void;
|
||||
onAuthenticationRequested(username: string, password: string, rememberMe: boolean): void;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & StateProps & DispatchProps;
|
||||
|
||||
interface State {
|
||||
username: string;
|
||||
password: string;
|
||||
rememberMe: boolean;
|
||||
}
|
||||
|
||||
|
@ -35,8 +37,6 @@ class FirstFactorForm extends Component<Props, State> {
|
|||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
rememberMe: false,
|
||||
}
|
||||
}
|
||||
|
@ -49,12 +49,12 @@ class FirstFactorForm extends Component<Props, State> {
|
|||
|
||||
onUsernameChanged = (e: FormEvent<HTMLElement>) => {
|
||||
const val = (e.target as HTMLInputElement).value;
|
||||
this.setState({username: val});
|
||||
this.props.onUsernameChanged(val);
|
||||
}
|
||||
|
||||
onPasswordChanged = (e: FormEvent<HTMLElement>) => {
|
||||
const val = (e.target as HTMLInputElement).value;
|
||||
this.setState({password: val});
|
||||
this.props.onPasswordChanged(val);
|
||||
}
|
||||
|
||||
onLoginClicked = () => {
|
||||
|
@ -83,9 +83,10 @@ class FirstFactorForm extends Component<Props, State> {
|
|||
outlined={true}>
|
||||
<Input
|
||||
id="username"
|
||||
name="username"
|
||||
onChange={this.onUsernameChanged}
|
||||
disabled={this.props.formDisabled}
|
||||
value={this.state.username}/>
|
||||
value={this.props.username}/>
|
||||
</TextField>
|
||||
</div>
|
||||
<div className={styles.field}>
|
||||
|
@ -95,11 +96,12 @@ class FirstFactorForm extends Component<Props, State> {
|
|||
outlined={true}>
|
||||
<Input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
disabled={this.props.formDisabled}
|
||||
onChange={this.onPasswordChanged}
|
||||
onKeyPress={this.onPasswordKeyPressed}
|
||||
value={this.state.password} />
|
||||
value={this.props.password} />
|
||||
</TextField>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -134,13 +136,9 @@ class FirstFactorForm extends Component<Props, State> {
|
|||
|
||||
private authenticate() {
|
||||
this.props.onAuthenticationRequested(
|
||||
this.state.username,
|
||||
this.state.password,
|
||||
this.props.username,
|
||||
this.props.password,
|
||||
this.state.rememberMe)
|
||||
.catch((err: Error) => console.error(err))
|
||||
.finally(() => {
|
||||
this.setState({username: '', password: ''});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { authenticateFailure, authenticateSuccess, authenticate } from '../../../reducers/Portal/FirstFactor/actions';
|
||||
import {
|
||||
authenticateFailure,
|
||||
authenticateSuccess,
|
||||
authenticate,
|
||||
setUsername,
|
||||
setPassword
|
||||
} from '../../../reducers/Portal/FirstFactor/actions';
|
||||
import FirstFactorForm, { StateProps, OwnProps } from '../../../components/FirstFactorForm/FirstFactorForm';
|
||||
import { RootState } from '../../../reducers';
|
||||
import to from 'await-to-js';
|
||||
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
|
||||
import AutheliaService from '../../../services/AutheliaService';
|
||||
|
||||
|
@ -11,57 +16,42 @@ const mapStateToProps = (state: RootState): StateProps => {
|
|||
return {
|
||||
error: state.firstFactor.error,
|
||||
formDisabled: state.firstFactor.loading,
|
||||
username: state.firstFactor.username,
|
||||
password: state.firstFactor.password,
|
||||
};
|
||||
}
|
||||
|
||||
function onAuthenticationRequested(dispatch: Dispatch, redirectionUrl: string | null) {
|
||||
return async (username: string, password: string, rememberMe: boolean): Promise<void> => {
|
||||
let err, res;
|
||||
|
||||
return async (username: string, password: string, rememberMe: boolean): Promise<void> => {
|
||||
// Validate first factor
|
||||
dispatch(authenticate());
|
||||
[err, res] = await to(AutheliaService.postFirstFactorAuth(
|
||||
username, password, rememberMe, redirectionUrl));
|
||||
|
||||
if (err) {
|
||||
await dispatch(authenticateFailure(err.message));
|
||||
throw new Error(err.message);
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
await dispatch(authenticateFailure('No response'));
|
||||
throw new Error('No response');
|
||||
}
|
||||
|
||||
if (res.status === 200) {
|
||||
const json = await res.json();
|
||||
if ('error' in json) {
|
||||
await dispatch(authenticateFailure(json['error']));
|
||||
throw new Error(json['error']);
|
||||
}
|
||||
|
||||
dispatch(authenticateSuccess());
|
||||
if ('redirect' in json) {
|
||||
window.location.href = json['redirect'];
|
||||
try {
|
||||
const redirectOrUndefined = await AutheliaService.postFirstFactorAuth(
|
||||
username, password, rememberMe, redirectionUrl);
|
||||
if (redirectOrUndefined) {
|
||||
window.location.href = redirectOrUndefined.redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(authenticateSuccess());
|
||||
dispatch(setUsername(''));
|
||||
dispatch(setPassword(''));
|
||||
// fetch state to move to next stage in case redirect is not possible
|
||||
await FetchStateBehavior(dispatch);
|
||||
} else if (res.status === 204) {
|
||||
dispatch(authenticateSuccess());
|
||||
|
||||
// fetch state to move to next stage
|
||||
await FetchStateBehavior(dispatch);
|
||||
} else {
|
||||
dispatch(authenticateFailure('Unknown error'));
|
||||
throw new Error('Unknown error... (' + res.status + ')');
|
||||
} catch (err) {
|
||||
dispatch(setPassword(''));
|
||||
dispatch(authenticateFailure(err.message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
|
||||
return {
|
||||
onUsernameChanged: function(username: string) {
|
||||
dispatch(setUsername(username));
|
||||
},
|
||||
onPasswordChanged: function(password: string) {
|
||||
dispatch(setPassword(password));
|
||||
},
|
||||
onAuthenticationRequested: onAuthenticationRequested(dispatch, ownProps.redirectionUrl),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { Dispatch } from 'redux';
|
|||
import SecondFactorDuoPush, { StateProps, OwnProps, DispatchProps } from '../../../components/SecondFactorDuoPush/SecondFactorDuoPush';
|
||||
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
|
||||
import TriggerDuoPushAuth from '../../../behaviors/TriggerDuoPushAuth';
|
||||
import RedirectionResponse from '../../../services/RedirectResponse';
|
||||
|
||||
|
||||
const mapStateToProps = (state: RootState): StateProps => ({
|
||||
|
@ -20,7 +19,7 @@ async function redirectIfPossible(body: any) {
|
|||
return false;
|
||||
}
|
||||
|
||||
async function handleSuccess(dispatch: Dispatch, body: RedirectionResponse | undefined, duration?: number) {
|
||||
async function handleSuccess(dispatch: Dispatch, body: {redirect: string} | undefined, duration?: number) {
|
||||
async function handle() {
|
||||
const redirected = await redirectIfPossible(body);
|
||||
if (!redirected) {
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
oneTimePasswordVerificationFailure,
|
||||
oneTimePasswordVerificationSuccess
|
||||
} from '../../../reducers/Portal/SecondFactor/actions';
|
||||
import to from 'await-to-js';
|
||||
import AutheliaService from '../../../services/AutheliaService';
|
||||
import { push } from 'connected-react-router';
|
||||
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
|
||||
|
@ -18,21 +17,6 @@ const mapStateToProps = (state: RootState): StateProps => ({
|
|||
oneTimePasswordVerificationError: state.secondFactor.oneTimePasswordVerificationError,
|
||||
});
|
||||
|
||||
async function redirectIfPossible(dispatch: Dispatch, res: Response) {
|
||||
if (res.status === 204) return;
|
||||
|
||||
const body = await res.json();
|
||||
if ('error' in body) {
|
||||
throw new Error(body['error']);
|
||||
}
|
||||
|
||||
if ('redirect' in body) {
|
||||
window.location.href = body['redirect'];
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function handleSuccess(dispatch: Dispatch, duration?: number) {
|
||||
async function handle() {
|
||||
await FetchStateBehavior(dispatch);
|
||||
|
@ -48,23 +32,17 @@ async function handleSuccess(dispatch: Dispatch, duration?: number) {
|
|||
const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
|
||||
return {
|
||||
onOneTimePasswordValidationRequested: async (token: string) => {
|
||||
let err, res;
|
||||
dispatch(oneTimePasswordVerification());
|
||||
[err, res] = await to(AutheliaService.verifyTotpToken(token, ownProps.redirectionUrl));
|
||||
if (err) {
|
||||
dispatch(oneTimePasswordVerificationFailure(err.message));
|
||||
throw err;
|
||||
}
|
||||
if (!res) {
|
||||
dispatch(oneTimePasswordVerificationFailure('No response'));
|
||||
throw 'No response';
|
||||
}
|
||||
|
||||
try {
|
||||
await redirectIfPossible(dispatch, res);
|
||||
dispatch(oneTimePasswordVerification());
|
||||
const response = await AutheliaService.verifyTotpToken(token, ownProps.redirectionUrl);
|
||||
dispatch(oneTimePasswordVerificationSuccess());
|
||||
if (response) {
|
||||
window.location.href = response.redirect;
|
||||
return;
|
||||
}
|
||||
await handleSuccess(dispatch);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
dispatch(oneTimePasswordVerificationFailure(err.message));
|
||||
}
|
||||
},
|
||||
|
|
|
@ -5,7 +5,6 @@ import SecondFactorU2F, { StateProps, OwnProps } from '../../../components/Secon
|
|||
import AutheliaService from '../../../services/AutheliaService';
|
||||
import { push } from 'connected-react-router';
|
||||
import u2fApi from 'u2f-api';
|
||||
import to from 'await-to-js';
|
||||
import {
|
||||
securityKeySignSuccess,
|
||||
securityKeySign,
|
||||
|
@ -20,58 +19,27 @@ const mapStateToProps = (state: RootState): StateProps => ({
|
|||
});
|
||||
|
||||
async function triggerSecurityKeySigning(dispatch: Dispatch, redirectionUrl: string | null) {
|
||||
let err, result;
|
||||
dispatch(securityKeySign());
|
||||
[err, result] = await to(AutheliaService.requestSigning());
|
||||
if (err) {
|
||||
await dispatch(securityKeySignFailure(err.message));
|
||||
throw err;
|
||||
const signRequest = await AutheliaService.requestSigning();
|
||||
const signRequests: u2fApi.SignRequest[] = [];
|
||||
for (var i in signRequest.registeredKeys) {
|
||||
const r = signRequest.registeredKeys[i];
|
||||
signRequests.push({
|
||||
appId: signRequest.appId,
|
||||
challenge: signRequest.challenge,
|
||||
keyHandle: r.keyHandle,
|
||||
version: r.version,
|
||||
})
|
||||
}
|
||||
const signResponse = await u2fApi.sign(signRequests, 60);
|
||||
const response = await AutheliaService.completeSecurityKeySigning(signResponse, redirectionUrl);
|
||||
dispatch(securityKeySignSuccess());
|
||||
|
||||
if (!result) {
|
||||
await dispatch(securityKeySignFailure('No response'));
|
||||
throw 'No response';
|
||||
}
|
||||
|
||||
[err, result] = await to(u2fApi.sign(result, 60));
|
||||
if (err) {
|
||||
await dispatch(securityKeySignFailure(err.message));
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
await dispatch(securityKeySignFailure('No response'));
|
||||
throw 'No response';
|
||||
}
|
||||
|
||||
[err, result] = await to(AutheliaService.completeSecurityKeySigning(result, redirectionUrl));
|
||||
if (err) {
|
||||
await dispatch(securityKeySignFailure(err.message));
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
await redirectIfPossible(result as Response);
|
||||
dispatch(securityKeySignSuccess());
|
||||
await handleSuccess(dispatch, 1000);
|
||||
} catch (err) {
|
||||
dispatch(securityKeySignFailure(err.message));
|
||||
}
|
||||
}
|
||||
|
||||
async function redirectIfPossible(res: Response) {
|
||||
if (res.status === 204) return;
|
||||
|
||||
const body = await res.json();
|
||||
if ('error' in body) {
|
||||
throw new Error(body['error']);
|
||||
}
|
||||
|
||||
if ('redirect' in body) {
|
||||
window.location.href = body['redirect'];
|
||||
if (response) {
|
||||
window.location.href = response.redirect;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
await handleSuccess(dispatch, 1000);
|
||||
}
|
||||
|
||||
async function handleSuccess(dispatch: Dispatch, duration?: number) {
|
||||
|
@ -93,7 +61,12 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
|
|||
await dispatch(push('/confirmation-sent'));
|
||||
},
|
||||
onInit: async () => {
|
||||
await triggerSecurityKeySigning(dispatch, ownProps.redirectionUrl);
|
||||
try {
|
||||
await triggerSecurityKeySigning(dispatch, ownProps.redirectionUrl);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
await dispatch(securityKeySignFailure(err.message));
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
|
|||
const params = QueryString.parse(ownProps.location.search);
|
||||
if ('rd' in params) {
|
||||
url = params['rd'] as string;
|
||||
} else if (state.authentication.remoteState && state.authentication.remoteState.default_redirection_url) {
|
||||
url = state.authentication.remoteState.default_redirection_url;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,48 +2,23 @@ import { connect } from 'react-redux';
|
|||
import OneTimePasswordRegistrationView from '../../../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView';
|
||||
import { RootState } from '../../../reducers';
|
||||
import { Dispatch } from 'redux';
|
||||
import {to} from 'await-to-js';
|
||||
import { generateTotpSecret, generateTotpSecretSuccess, generateTotpSecretFailure } from '../../../reducers/Portal/OneTimePasswordRegistration/actions';
|
||||
import { push } from 'connected-react-router';
|
||||
import AutheliaService from '../../../services/AutheliaService';
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
error: state.oneTimePasswordRegistration.error,
|
||||
secret: state.oneTimePasswordRegistration.secret,
|
||||
});
|
||||
|
||||
async function checkIdentity(token: string) {
|
||||
return fetch(`/api/secondfactor/totp/identity/finish?token=${token}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Status code ' + res.status);
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
if ('error' in body) {
|
||||
throw new Error(body['error']);
|
||||
}
|
||||
return body;
|
||||
});
|
||||
}
|
||||
|
||||
async function tryGenerateTotpSecret(dispatch: Dispatch, token: string) {
|
||||
let err, result;
|
||||
dispatch(generateTotpSecret());
|
||||
[err, result] = await to(checkIdentity(token));
|
||||
if (err) {
|
||||
const e = err;
|
||||
setTimeout(() => {
|
||||
dispatch(generateTotpSecretFailure(e.message));
|
||||
}, 2000);
|
||||
return;
|
||||
try {
|
||||
dispatch(generateTotpSecret());
|
||||
const res = await AutheliaService.completeOneTimePasswordRegistrationIdentityValidation(token);
|
||||
dispatch(generateTotpSecretSuccess(res));
|
||||
} catch (err) {
|
||||
dispatch(generateTotpSecretFailure(err.message));
|
||||
}
|
||||
dispatch(generateTotpSecretSuccess(result));
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||
|
|
|
@ -12,11 +12,19 @@ const mapStateToProps = (state: RootState): StateProps => ({
|
|||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||
return {
|
||||
onInit: async (token: string) => {
|
||||
await AutheliaService.completePasswordResetIdentityValidation(token);
|
||||
try {
|
||||
await AutheliaService.completePasswordResetIdentityValidation(token);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
onPasswordResetRequested: async (newPassword: string) => {
|
||||
await AutheliaService.resetPassword(newPassword);
|
||||
await dispatch(push('/'));
|
||||
try {
|
||||
await AutheliaService.resetPassword(newPassword);
|
||||
await dispatch(push('/'));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
onCancelClicked: async () => {
|
||||
await dispatch(push('/'));
|
||||
|
|
|
@ -12,26 +12,30 @@ const mapStateToProps = (state: RootState) => ({
|
|||
error: state.securityKeyRegistration.error,
|
||||
});
|
||||
|
||||
function fail(dispatch: Dispatch, err: Error) {
|
||||
console.error(err);
|
||||
dispatch(registerSecurityKeyFailure(err.message));
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => {
|
||||
return {
|
||||
onInit: async (token: string) => {
|
||||
try {
|
||||
dispatch(registerSecurityKey());
|
||||
await AutheliaService.completeSecurityKeyRegistrationIdentityValidation(token);
|
||||
const registerRequest = await AutheliaService.requestSecurityKeyRegistration();
|
||||
const registerResponse = await U2fApi.register([registerRequest], [], 60);
|
||||
const registerRequest = await AutheliaService.completeSecurityKeyRegistrationIdentityValidation(token);
|
||||
const registerRequests: U2fApi.RegisterRequest[] = [];
|
||||
for(var i in registerRequest.registerRequests) {
|
||||
const r = registerRequest.registerRequests[i];
|
||||
registerRequests.push({
|
||||
appId: registerRequest.appId,
|
||||
challenge: r.challenge,
|
||||
version: r.version,
|
||||
})
|
||||
}
|
||||
const registerResponse = await U2fApi.register(registerRequests, [], 60);
|
||||
await AutheliaService.completeSecurityKeyRegistration(registerResponse);
|
||||
dispatch(registerSecurityKeySuccess());
|
||||
setTimeout(() => {
|
||||
ownProps.history.push('/');
|
||||
}, 2000);
|
||||
} catch(err) {
|
||||
fail(dispatch, err);
|
||||
console.error(err);
|
||||
dispatch(registerSecurityKeyFailure(err.message));
|
||||
}
|
||||
},
|
||||
onBackClicked: () => {
|
||||
|
|
|
@ -2,7 +2,9 @@ import { createAction } from 'typesafe-actions';
|
|||
import {
|
||||
AUTHENTICATE_REQUEST,
|
||||
AUTHENTICATE_SUCCESS,
|
||||
AUTHENTICATE_FAILURE
|
||||
AUTHENTICATE_FAILURE,
|
||||
FIRST_FACTOR_SET_USERNAME,
|
||||
FIRST_FACTOR_SET_PASSWORD
|
||||
} from "../../constants";
|
||||
|
||||
/* AUTHENTICATE_REQUEST */
|
||||
|
@ -11,3 +13,11 @@ export const authenticateSuccess = createAction(AUTHENTICATE_SUCCESS);
|
|||
export const authenticateFailure = createAction(AUTHENTICATE_FAILURE, resolve => {
|
||||
return (error: string) => resolve(error);
|
||||
});
|
||||
|
||||
|
||||
export const setUsername = createAction(FIRST_FACTOR_SET_USERNAME, resolve => {
|
||||
return (username: string) => resolve(username);
|
||||
});
|
||||
export const setPassword = createAction(FIRST_FACTOR_SET_PASSWORD, resolve => {
|
||||
return (password: string) => resolve(password);
|
||||
});
|
|
@ -14,12 +14,16 @@ interface FirstFactorState {
|
|||
lastResult: Result;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const firstFactorInitialState: FirstFactorState = {
|
||||
lastResult: Result.NONE,
|
||||
loading: false,
|
||||
error: null,
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
|
||||
export default (state = firstFactorInitialState, action: FirstFactorAction): FirstFactorState => {
|
||||
|
@ -44,6 +48,16 @@ export default (state = firstFactorInitialState, action: FirstFactorAction): Fir
|
|||
loading: false,
|
||||
error: action.payload,
|
||||
};
|
||||
case getType(Actions.setUsername):
|
||||
return {
|
||||
...state,
|
||||
username: action.payload,
|
||||
}
|
||||
case getType(Actions.setPassword):
|
||||
return {
|
||||
...state,
|
||||
password: action.payload,
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
|
@ -4,9 +4,12 @@ export const FETCH_STATE_SUCCESS = '@portal/fetch_state_success';
|
|||
export const FETCH_STATE_FAILURE = '@portal/fetch_state_failure';
|
||||
|
||||
// AUTHENTICATION PROCESS
|
||||
export const AUTHENTICATE_REQUEST = '@portal/authenticate_request';
|
||||
export const AUTHENTICATE_SUCCESS = '@portal/authenticate_success';
|
||||
export const AUTHENTICATE_FAILURE = '@portal/authenticate_failure';
|
||||
export const FIRST_FACTOR_SET_USERNAME = "@portal/first_factor/set_username";
|
||||
export const FIRST_FACTOR_SET_PASSWORD = "@portal/first_factor/set_password";
|
||||
|
||||
export const AUTHENTICATE_REQUEST = '@portal/first_factor/authenticate_request';
|
||||
export const AUTHENTICATE_SUCCESS = '@portal/first_factor/authenticate_success';
|
||||
export const AUTHENTICATE_FAILURE = '@portal/first_factor/authenticate_failure';
|
||||
|
||||
// SECOND FACTOR PAGE
|
||||
export const SET_SECURITY_KEY_SUPPORTED = '@portal/second_factor/set_security_key_supported';
|
||||
|
|
|
@ -4,6 +4,7 @@ import SecurityKeyRegistrationView from "../containers/views/SecurityKeyRegistra
|
|||
import ForgotPasswordView from "../containers/views/ForgotPasswordView/ForgotPasswordView";
|
||||
import ResetPasswordView from "../containers/views/ResetPasswordView/ResetPasswordView";
|
||||
import AuthenticationView from "../containers/views/AuthenticationView/AuthenticationView";
|
||||
import LogoutView from "../views/LogoutView/LogoutView";
|
||||
|
||||
export const routes = [{
|
||||
path: '/',
|
||||
|
@ -29,4 +30,8 @@ export const routes = [{
|
|||
path: '/reset-password',
|
||||
title: 'Reset password',
|
||||
component: ResetPasswordView,
|
||||
}, {
|
||||
path: '/logout',
|
||||
title: 'Logout',
|
||||
component: LogoutView,
|
||||
}]
|
|
@ -1,58 +1,73 @@
|
|||
import RemoteState from "../views/AuthenticationView/RemoteState";
|
||||
import U2fApi, { SignRequest } from "u2f-api";
|
||||
import U2fApi from "u2f-api";
|
||||
import Method2FA from "../types/Method2FA";
|
||||
import RedirectResponse from "./RedirectResponse";
|
||||
import PreferedMethodResponse from "./PreferedMethodResponse";
|
||||
import { string } from "prop-types";
|
||||
|
||||
interface DataResponse<T> {
|
||||
status: "OK";
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface ErrorResponse {
|
||||
status: "KO";
|
||||
message: string;
|
||||
}
|
||||
|
||||
type ServiceResponse<T> = DataResponse<T> | ErrorResponse;
|
||||
|
||||
class AutheliaService {
|
||||
static async fetchSafe(url: string, options?: RequestInit): Promise<Response> {
|
||||
const res = await fetch(url, options);
|
||||
if (res.status !== 200 && res.status !== 204) {
|
||||
throw new Error('Status code ' + res.status);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static async fetchSafeJson<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(url, options);
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Status code ' + res.status);
|
||||
}
|
||||
return await res.json();
|
||||
const response: ServiceResponse<T> = await res.json();
|
||||
if (response.status == "OK") {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch current authentication state.
|
||||
*/
|
||||
static async fetchState(): Promise<RemoteState> {
|
||||
return await this.fetchSafeJson('/api/state')
|
||||
return await this.fetchSafeJson<RemoteState>('/api/state')
|
||||
}
|
||||
|
||||
static async postFirstFactorAuth(username: string, password: string,
|
||||
rememberMe: boolean, redirectionUrl: string | null) {
|
||||
rememberMe: boolean, targetURL: string | null) {
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
if (redirectionUrl) {
|
||||
headers['X-Target-Url'] = redirectionUrl;
|
||||
const requestBody: {
|
||||
username: string,
|
||||
password: string,
|
||||
keepMeLoggedIn: boolean,
|
||||
targetURL?: string
|
||||
} = {
|
||||
username: username,
|
||||
password: password,
|
||||
keepMeLoggedIn: rememberMe,
|
||||
}
|
||||
|
||||
return this.fetchSafe('/api/firstfactor', {
|
||||
if (targetURL) {
|
||||
requestBody.targetURL = targetURL;
|
||||
}
|
||||
|
||||
return this.fetchSafeJson<{redirect: string}|undefined>('/api/firstfactor', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
password: password,
|
||||
keepMeLoggedIn: rememberMe,
|
||||
})
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
}
|
||||
|
||||
static async postLogout() {
|
||||
return this.fetchSafe('/api/logout', {
|
||||
return this.fetchSafeJson<undefined>('/api/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
|
@ -62,81 +77,81 @@ class AutheliaService {
|
|||
}
|
||||
|
||||
static async startU2FRegistrationIdentityProcess() {
|
||||
return this.fetchSafe('/api/secondfactor/u2f/identity/start', {
|
||||
return this.fetchSafeJson<undefined>('/api/secondfactor/u2f/identity/start', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
static async startTOTPRegistrationIdentityProcess() {
|
||||
return this.fetchSafe('/api/secondfactor/totp/identity/start', {
|
||||
return this.fetchSafeJson<undefined>('/api/secondfactor/totp/identity/start', {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
static async requestSigning() {
|
||||
return this.fetchSafeJson<SignRequest>('/api/u2f/sign_request');
|
||||
return this.fetchSafeJson<{
|
||||
appId: string,
|
||||
challenge: string,
|
||||
registeredKeys: {
|
||||
appId: string,
|
||||
keyHandle: string,
|
||||
version: string,
|
||||
}[]
|
||||
}>('/api/secondfactor/u2f/sign_request', {
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
static async completeSecurityKeySigning(
|
||||
response: U2fApi.SignResponse, redirectionUrl: string | null) {
|
||||
response: U2fApi.SignResponse, targetURL: string | null) {
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
const headers: Record<string, string> = {'Content-Type': 'application/json',}
|
||||
const requestBody: {signResponse: U2fApi.SignResponse, targetURL?: string} = {
|
||||
signResponse: response,
|
||||
};
|
||||
if (targetURL) {
|
||||
requestBody.targetURL = targetURL;
|
||||
}
|
||||
if (redirectionUrl) {
|
||||
headers['X-Target-Url'] = redirectionUrl;
|
||||
}
|
||||
return this.fetchSafe('/api/u2f/sign', {
|
||||
return this.fetchSafeJson<{redirect: string}|undefined>('/api/secondfactor/u2f/sign', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(response),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
}
|
||||
|
||||
static async verifyTotpToken(
|
||||
token: string, redirectionUrl: string | null) {
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
token: string, targetURL: string | null) {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (redirectionUrl) {
|
||||
headers['X-Target-Url'] = redirectionUrl;
|
||||
var requestBody: {token: string, targetURL?: string} = {token};
|
||||
if (targetURL) {
|
||||
requestBody.targetURL = targetURL;
|
||||
}
|
||||
return this.fetchSafe('/api/totp', {
|
||||
return this.fetchSafeJson<{redirect: string}|undefined>('/api/secondfactor/totp', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({token}),
|
||||
body: JSON.stringify(requestBody),
|
||||
})
|
||||
}
|
||||
|
||||
static async triggerDuoPush(redirectionUrl: string | null): Promise<RedirectResponse | undefined> {
|
||||
static async triggerDuoPush(targetURL: string | null): Promise<{redirect: string}|undefined> {
|
||||
const headers: Record<string, string> = {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (redirectionUrl) {
|
||||
headers['X-Target-Url'] = redirectionUrl;
|
||||
const requestBody: {targetURL?: string} = {}
|
||||
if (targetURL) {
|
||||
requestBody.targetURL = targetURL;
|
||||
}
|
||||
const res = await this.fetchSafe('/api/duo-push', {
|
||||
return this.fetchSafeJson<{redirect: string}|undefined>('/api/secondfactor/duo', {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (res.status === 204) {
|
||||
return;
|
||||
}
|
||||
|
||||
const body = await res.json();
|
||||
if ('error' in body) {
|
||||
throw new Error(body['error']);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
static async initiatePasswordResetIdentityValidation(username: string) {
|
||||
return this.fetchSafe('/api/password-reset/identity/start', {
|
||||
return this.fetchSafeJson<undefined>('/api/reset-password/identity/start', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
|
@ -147,13 +162,17 @@ class AutheliaService {
|
|||
}
|
||||
|
||||
static async completePasswordResetIdentityValidation(token: string) {
|
||||
return fetch(`/api/password-reset/identity/finish?token=${token}`, {
|
||||
return this.fetchSafeJson<undefined>(`/api/reset-password/identity/finish`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({token})
|
||||
});
|
||||
}
|
||||
|
||||
static async resetPassword(newPassword: string) {
|
||||
return this.fetchSafe('/api/password-reset', {
|
||||
return this.fetchSafeJson<undefined>('/api/reset-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
|
@ -164,27 +183,14 @@ class AutheliaService {
|
|||
}
|
||||
|
||||
static async fetchPrefered2faMethod(): Promise<Method2FA> {
|
||||
const doc = await this.fetchSafeJson<PreferedMethodResponse>('/api/secondfactor/preferences');
|
||||
if (!doc) {
|
||||
throw new Error("No response.");
|
||||
}
|
||||
|
||||
if (doc.error) {
|
||||
throw new Error(doc.error);
|
||||
}
|
||||
|
||||
if (!doc.method) {
|
||||
throw new Error("No method.");
|
||||
}
|
||||
|
||||
return doc.method;
|
||||
const res = await this.fetchSafeJson<{method: Method2FA}>('/api/secondfactor/preferences');
|
||||
return res.method;
|
||||
}
|
||||
|
||||
static async setPrefered2faMethod(method: Method2FA): Promise<void> {
|
||||
await this.fetchSafe('/api/secondfactor/preferences', {
|
||||
return this.fetchSafeJson<undefined>('/api/secondfactor/preferences', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({method})
|
||||
|
@ -192,11 +198,12 @@ class AutheliaService {
|
|||
}
|
||||
|
||||
static async getAvailable2faMethods(): Promise<Method2FA[]> {
|
||||
return await this.fetchSafeJson('/api/secondfactor/available');
|
||||
return this.fetchSafeJson('/api/secondfactor/available');
|
||||
}
|
||||
|
||||
static async completeSecurityKeyRegistration(response: U2fApi.RegisterResponse): Promise<Response> {
|
||||
return await this.fetchSafe('/api/u2f/register', {
|
||||
static async completeSecurityKeyRegistration(
|
||||
response: U2fApi.RegisterResponse): Promise<undefined> {
|
||||
return this.fetchSafeJson('/api/secondfactor/u2f/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
|
@ -206,19 +213,30 @@ class AutheliaService {
|
|||
});
|
||||
}
|
||||
|
||||
static async requestSecurityKeyRegistration() {
|
||||
return this.fetchSafeJson<U2fApi.RegisterRequest>('/api/u2f/register_request')
|
||||
static async completeSecurityKeyRegistrationIdentityValidation(token: string) {
|
||||
return this.fetchSafeJson<{
|
||||
appId: string,
|
||||
registerRequests: [{
|
||||
version: string,
|
||||
challenge: string,
|
||||
}]
|
||||
}>(`/api/secondfactor/u2f/identity/finish`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({token})
|
||||
});
|
||||
}
|
||||
|
||||
static async completeSecurityKeyRegistrationIdentityValidation(token: string) {
|
||||
const res = await this.fetchSafeJson(`/api/secondfactor/u2f/identity/finish?token=${token}`, {
|
||||
static async completeOneTimePasswordRegistrationIdentityValidation(token: string) {
|
||||
return this.fetchSafeJson<{base32_secret: string, otpauth_url: string}>(`/api/secondfactor/totp/identity/finish`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({token})
|
||||
});
|
||||
|
||||
if ('error' in res) {
|
||||
throw new Error(res['error']);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
|
||||
export default interface RedirectResponse {
|
||||
redirect?: string;
|
||||
error?: string;
|
||||
}
|
16
client/src/views/LogoutView/LogoutView.tsx
Normal file
16
client/src/views/LogoutView/LogoutView.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React from "react"
|
||||
import { Redirect } from "react-router";
|
||||
|
||||
async function logout() {
|
||||
return fetch("/api/logout", {method: "POST"})
|
||||
}
|
||||
|
||||
export default class LogoutView extends React.Component {
|
||||
componentDidMount() {
|
||||
logout().catch(console.error);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <Redirect to='/' />;
|
||||
}
|
||||
}
|
|
@ -10,6 +10,10 @@ port: 9091
|
|||
# Level of verbosity for logs
|
||||
logs_level: debug
|
||||
|
||||
# The secret used to generate JWT tokens when validating user identity by
|
||||
# email confirmation.
|
||||
jwt_secret: a_very_important_secret
|
||||
|
||||
# Default redirection URL
|
||||
#
|
||||
# If user tries to authenticate without any referer, Authelia
|
||||
|
@ -263,19 +267,20 @@ notifier:
|
|||
## filesystem:
|
||||
## filename: /tmp/authelia/notification.txt
|
||||
|
||||
# Use your email account to send the notifications. You can use an app password.
|
||||
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/
|
||||
## email:
|
||||
## username: user@example.com
|
||||
## password: yourpassword
|
||||
## sender: admin@example.com
|
||||
## service: gmail
|
||||
|
||||
# Use a SMTP server for sending notifications
|
||||
# Use a SMTP server for sending notifications. Authelia uses PLAIN method to authenticate.
|
||||
# [Security] Make sure the connection is made over TLS otherwise your password will transit in plain text.
|
||||
smtp:
|
||||
username: test
|
||||
password: password
|
||||
secure: false
|
||||
host: 127.0.0.1
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
|
||||
# Sending an email using a Gmail account is as simple as the next section.
|
||||
# You need to create an app password by following: https://support.google.com/accounts/answer/185833?hl=en
|
||||
## smtp:
|
||||
## username: myaccount@gmail.com
|
||||
## password: yourapppassword
|
||||
## sender: admin@example.com
|
||||
## host: smtp.gmail.com
|
||||
## port: 587
|
||||
|
|
39
configuration/reader.go
Normal file
39
configuration/reader.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package configuration
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
"github.com/clems4ever/authelia/configuration/validator"
|
||||
)
|
||||
|
||||
func check(e error) {
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Read a YAML configuration and create a Configuration object out of it.
|
||||
func Read(configPath string) (*schema.Configuration, []error) {
|
||||
config := schema.Configuration{}
|
||||
|
||||
data, err := ioutil.ReadFile(configPath)
|
||||
check(err)
|
||||
|
||||
err = yaml.Unmarshal([]byte(data), &config)
|
||||
|
||||
if err != nil {
|
||||
return nil, []error{err}
|
||||
}
|
||||
|
||||
val := schema.NewStructValidator()
|
||||
validator.Validate(&config, val)
|
||||
|
||||
if val.HasErrors() {
|
||||
return nil, val.Errors()
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
22
configuration/reader_test.go
Normal file
22
configuration/reader_test.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package configuration
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestShouldParseConfigFile(t *testing.T) {
|
||||
config, errors := Read("../test-resources/config.yml")
|
||||
|
||||
assert.Len(t, errors, 0)
|
||||
|
||||
assert.Equal(t, 9091, config.Port)
|
||||
assert.Equal(t, "debug", config.LogsLevel)
|
||||
assert.Equal(t, "https://home.example.com:8080/", config.DefaultRedirectionURL)
|
||||
assert.Equal(t, "authelia.com", config.TOTP.Issuer)
|
||||
|
||||
assert.Equal(t, "api-123456789.example.com", config.DuoAPI.Hostname)
|
||||
assert.Equal(t, "ABCDEF", config.DuoAPI.IntegrationKey)
|
||||
assert.Equal(t, "1234567890abcdefghifjkl", config.DuoAPI.SecretKey)
|
||||
}
|
70
configuration/schema/access_control.go
Normal file
70
configuration/schema/access_control.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ACLRule represent one ACL rule
|
||||
type ACLRule struct {
|
||||
Domain string `yaml:"domain"`
|
||||
Policy string `yaml:"policy"`
|
||||
Subject string `yaml:"subject"`
|
||||
Networks []string `yaml:"networks"`
|
||||
Resources []string `yaml:"resources"`
|
||||
}
|
||||
|
||||
// IsPolicyValid check if policy is valid
|
||||
func IsPolicyValid(policy string) bool {
|
||||
return policy == "deny" || policy == "one_factor" || policy == "two_factor" || policy == "bypass"
|
||||
}
|
||||
|
||||
// IsSubjectValid check if a subject is valid
|
||||
func IsSubjectValid(subject string) bool {
|
||||
return subject == "" || strings.HasPrefix(subject, "user:") || strings.HasPrefix(subject, "group:")
|
||||
}
|
||||
|
||||
// IsNetworkValid check if a network is valid
|
||||
func IsNetworkValid(network string) bool {
|
||||
_, _, err := net.ParseCIDR(network)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Validate validate an ACL Rule
|
||||
func (r *ACLRule) Validate(validator *StructValidator) {
|
||||
if r.Domain == "" {
|
||||
validator.Push(fmt.Errorf("Domain must be provided"))
|
||||
}
|
||||
|
||||
if !IsPolicyValid(r.Policy) {
|
||||
validator.Push(fmt.Errorf("A policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'"))
|
||||
}
|
||||
|
||||
if !IsSubjectValid(r.Subject) {
|
||||
validator.Push(fmt.Errorf("A subject must start with 'user:' or 'group:'"))
|
||||
}
|
||||
|
||||
for i, network := range r.Networks {
|
||||
if !IsNetworkValid(network) {
|
||||
validator.Push(fmt.Errorf("Network %d must be a valid CIDR", i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AccessControlConfiguration represents the configuration related to ACLs.
|
||||
type AccessControlConfiguration struct {
|
||||
DefaultPolicy string `yaml:"default_policy"`
|
||||
Rules []ACLRule `yaml:"rules"`
|
||||
}
|
||||
|
||||
// Validate validate the access control configuration
|
||||
func (acc *AccessControlConfiguration) Validate(validator *StructValidator) {
|
||||
if acc.DefaultPolicy == "" {
|
||||
acc.DefaultPolicy = "deny"
|
||||
}
|
||||
|
||||
if !IsPolicyValid(acc.DefaultPolicy) {
|
||||
validator.Push(fmt.Errorf("'default_policy' must either be 'deny', 'two_factor', 'one_factor' or 'bypass'"))
|
||||
}
|
||||
}
|
26
configuration/schema/authentication.go
Normal file
26
configuration/schema/authentication.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package schema
|
||||
|
||||
// LDAPAuthenticationBackendConfiguration represents the configuration related to LDAP server.
|
||||
type LDAPAuthenticationBackendConfiguration struct {
|
||||
URL string `yaml:"url"`
|
||||
BaseDN string `yaml:"base_dn"`
|
||||
AdditionalUsersDN string `yaml:"additional_users_dn"`
|
||||
UsersFilter string `yaml:"users_filter"`
|
||||
AdditionalGroupsDN string `yaml:"additional_groups_dn"`
|
||||
GroupsFilter string `yaml:"groups_filter"`
|
||||
GroupNameAttribute string `yaml:"group_name_attribute"`
|
||||
MailAttribute string `yaml:"mail_attribute"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
}
|
||||
|
||||
// FileAuthenticationBackendConfiguration represents the configuration related to file-based backend
|
||||
type FileAuthenticationBackendConfiguration struct {
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
// AuthenticationBackendConfiguration represents the configuration related to the authentication backend.
|
||||
type AuthenticationBackendConfiguration struct {
|
||||
Ldap *LDAPAuthenticationBackendConfiguration `yaml:"ldap"`
|
||||
File *FileAuthenticationBackendConfiguration `yaml:"file"`
|
||||
}
|
18
configuration/schema/configuration.go
Normal file
18
configuration/schema/configuration.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package schema
|
||||
|
||||
// Configuration object extracted from YAML configuration file.
|
||||
type Configuration struct {
|
||||
Port int `yaml:"port"`
|
||||
LogsLevel string `yaml:"logs_level"`
|
||||
JWTSecret string `yaml:"jwt_secret"`
|
||||
DefaultRedirectionURL string `yaml:"default_redirection_url"`
|
||||
AuthenticationBackend AuthenticationBackendConfiguration `yaml:"authentication_backend"`
|
||||
Session SessionConfiguration `yaml:"session"`
|
||||
|
||||
TOTP *TOTPConfiguration `yaml:"totp"`
|
||||
DuoAPI *DuoAPIConfiguration `yaml:"duo_api"`
|
||||
AccessControl *AccessControlConfiguration `yaml:"access_control"`
|
||||
Regulation *RegulationConfiguration `yaml:"regulation"`
|
||||
Storage *StorageConfiguration `yaml:"storage"`
|
||||
Notifier *NotifierConfiguration `yaml:"notifier"`
|
||||
}
|
8
configuration/schema/duo.go
Normal file
8
configuration/schema/duo.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package schema
|
||||
|
||||
// DuoAPIConfiguration represents the configuration related to Duo API.
|
||||
type DuoAPIConfiguration struct {
|
||||
Hostname string `yaml:"hostname"`
|
||||
IntegrationKey string `yaml:"integration_key"`
|
||||
SecretKey string `yaml:"secret_key"`
|
||||
}
|
31
configuration/schema/notifier.go
Normal file
31
configuration/schema/notifier.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
package schema
|
||||
|
||||
// FileSystemNotifierConfiguration represents the configuration of the notifier writing emails in a file.
|
||||
type FileSystemNotifierConfiguration struct {
|
||||
Filename string `yaml:"filename"`
|
||||
}
|
||||
|
||||
// EmailNotifierConfiguration represents the configuration of the email service notifier (like GMAIL API).
|
||||
type EmailNotifierConfiguration struct {
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Sender string `yaml:"sender"`
|
||||
Service string `yaml:"service"`
|
||||
}
|
||||
|
||||
// SMTPNotifierConfiguration represents the configuration of the SMTP server to send emails with.
|
||||
type SMTPNotifierConfiguration struct {
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Secure string `yaml:"secure"`
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
Sender string `yaml:"sender"`
|
||||
}
|
||||
|
||||
// NotifierConfiguration representes the configuration of the notifier to use when sending notifications to users.
|
||||
type NotifierConfiguration struct {
|
||||
FileSystem *FileSystemNotifierConfiguration `yaml:"filesystem"`
|
||||
Email *EmailNotifierConfiguration `yaml:"email"`
|
||||
SMTP *SMTPNotifierConfiguration `yaml:"smtp"`
|
||||
}
|
8
configuration/schema/regulation.go
Normal file
8
configuration/schema/regulation.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package schema
|
||||
|
||||
// RegulationConfiguration represents the configuration related to regulation.
|
||||
type RegulationConfiguration struct {
|
||||
MaxRetries int `yaml:"max_retries"`
|
||||
FindTime int64 `yaml:"find_time"`
|
||||
BanTime int64 `yaml:"ban_time"`
|
||||
}
|
26
configuration/schema/session.go
Normal file
26
configuration/schema/session.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package schema
|
||||
|
||||
// RedisSessionConfiguration represents the configuration related to redis session store.
|
||||
type RedisSessionConfiguration struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int64 `yaml:"port"`
|
||||
Password string `yaml:"password"`
|
||||
}
|
||||
|
||||
// SessionConfiguration represents the configuration related to user sessions.
|
||||
type SessionConfiguration struct {
|
||||
Name string `yaml:"name"`
|
||||
Secret string `yaml:"secret"`
|
||||
// Expiration in seconds
|
||||
Expiration int64 `yaml:"expiration"`
|
||||
// Inactivity in seconds
|
||||
Inactivity int64 `yaml:"inactivity"`
|
||||
Domain string `yaml:"domain"`
|
||||
Redis *RedisSessionConfiguration `yaml:"redis"`
|
||||
}
|
||||
|
||||
// DefaultSessionConfiguration is the default session configuration
|
||||
var DefaultSessionConfiguration = SessionConfiguration{
|
||||
Name: "authelia_session",
|
||||
Expiration: 3600,
|
||||
}
|
22
configuration/schema/storage.go
Normal file
22
configuration/schema/storage.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package schema
|
||||
|
||||
// MongoStorageConfiguration represents the configuration related to mongo connection.
|
||||
type MongoStorageConfiguration struct {
|
||||
URL string `yaml:"url"`
|
||||
Database string `yaml:"database"`
|
||||
Auth struct {
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
} `yaml:"auth"`
|
||||
}
|
||||
|
||||
// LocalStorageConfiguration represents the configuration when using local storage.
|
||||
type LocalStorageConfiguration struct {
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
// StorageConfiguration represents the configuration of the storage backend.
|
||||
type StorageConfiguration struct {
|
||||
Mongo *MongoStorageConfiguration `yaml:"mongo"`
|
||||
Local *LocalStorageConfiguration `yaml:"local"`
|
||||
}
|
6
configuration/schema/totp.go
Normal file
6
configuration/schema/totp.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package schema
|
||||
|
||||
// TOTPConfiguration represents the configuration related to TOTP options.
|
||||
type TOTPConfiguration struct {
|
||||
Issuer string
|
||||
}
|
129
configuration/schema/validator.go
Normal file
129
configuration/schema/validator.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/Workiva/go-datastructures/queue"
|
||||
)
|
||||
|
||||
// ErrorContainer represents a container where we can add errors and retrieve them
|
||||
type ErrorContainer interface {
|
||||
Push(err error)
|
||||
HasErrors() bool
|
||||
Errors() []error
|
||||
}
|
||||
|
||||
// Validator represents the validator interface
|
||||
type Validator struct {
|
||||
errors map[string][]error
|
||||
}
|
||||
|
||||
// NewValidator create a validator
|
||||
func NewValidator() *Validator {
|
||||
validator := new(Validator)
|
||||
validator.errors = make(map[string][]error)
|
||||
return validator
|
||||
}
|
||||
|
||||
// QueueItem an item representing a struct field and its path.
|
||||
type QueueItem struct {
|
||||
value reflect.Value
|
||||
path string
|
||||
}
|
||||
|
||||
func (v *Validator) validateOne(item QueueItem, q *queue.Queue) error {
|
||||
if item.value.Type().Kind() == reflect.Ptr {
|
||||
if item.value.IsNil() {
|
||||
return nil
|
||||
}
|
||||
|
||||
elem := item.value.Elem()
|
||||
q.Put(QueueItem{
|
||||
value: elem,
|
||||
path: item.path,
|
||||
})
|
||||
} else if item.value.Kind() == reflect.Struct {
|
||||
numFields := item.value.Type().NumField()
|
||||
|
||||
validateFn := item.value.Addr().MethodByName("Validate")
|
||||
|
||||
if validateFn.IsValid() {
|
||||
structValidator := NewStructValidator()
|
||||
validateFn.Call([]reflect.Value{reflect.ValueOf(structValidator)})
|
||||
v.errors[item.path] = structValidator.Errors()
|
||||
}
|
||||
|
||||
for i := 0; i < numFields; i++ {
|
||||
field := item.value.Type().Field(i)
|
||||
value := item.value.Field(i)
|
||||
|
||||
q.Put(QueueItem{
|
||||
value: value,
|
||||
path: item.path + "." + field.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validate a struct
|
||||
func (v *Validator) Validate(s interface{}) error {
|
||||
q := queue.New(40)
|
||||
q.Put(QueueItem{value: reflect.ValueOf(s), path: "root"})
|
||||
|
||||
for !q.Empty() {
|
||||
val, err := q.Get(1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
item, ok := val[0].(QueueItem)
|
||||
if !ok {
|
||||
return fmt.Errorf("Cannot convert item into QueueItem")
|
||||
}
|
||||
v.validateOne(item, q)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintErrors display the errors thrown during validation
|
||||
func (v *Validator) PrintErrors() {
|
||||
for path, errs := range v.errors {
|
||||
fmt.Printf("Errors at %s:\n", path)
|
||||
for _, err := range errs {
|
||||
fmt.Printf("--> %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Errors return the errors thrown during validation
|
||||
func (v *Validator) Errors() map[string][]error {
|
||||
return v.errors
|
||||
}
|
||||
|
||||
// StructValidator is a validator for structs
|
||||
type StructValidator struct {
|
||||
errors []error
|
||||
}
|
||||
|
||||
// NewStructValidator is a constructor of struct validator
|
||||
func NewStructValidator() *StructValidator {
|
||||
val := new(StructValidator)
|
||||
val.errors = make([]error, 0)
|
||||
return val
|
||||
}
|
||||
|
||||
// Push an error in the validator.
|
||||
func (v *StructValidator) Push(err error) {
|
||||
v.errors = append(v.errors, err)
|
||||
}
|
||||
|
||||
// HasErrors checks whether the validator contains errors.
|
||||
func (v *StructValidator) HasErrors() bool {
|
||||
return len(v.errors) > 0
|
||||
}
|
||||
|
||||
// Errors returns the errors.
|
||||
func (v *StructValidator) Errors() []error {
|
||||
return v.errors
|
||||
}
|
81
configuration/schema/validator_test.go
Normal file
81
configuration/schema/validator_test.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package schema_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
)
|
||||
|
||||
type TestNestedStruct struct {
|
||||
MustBe5 int
|
||||
}
|
||||
|
||||
func (tns *TestNestedStruct) Validate(validator *schema.StructValidator) {
|
||||
if tns.MustBe5 != 5 {
|
||||
validator.Push(fmt.Errorf("MustBe5 must be 5"))
|
||||
}
|
||||
}
|
||||
|
||||
type TestStruct struct {
|
||||
MustBe10 int
|
||||
NotEmpty string
|
||||
SetDefault string
|
||||
Nested TestNestedStruct
|
||||
Nested2 TestNestedStruct
|
||||
NilPtr *int
|
||||
NestedPtr *TestNestedStruct
|
||||
}
|
||||
|
||||
func (ts *TestStruct) Validate(validator *schema.StructValidator) {
|
||||
if ts.MustBe10 != 10 {
|
||||
validator.Push(fmt.Errorf("MustBe10 must be 10"))
|
||||
}
|
||||
|
||||
if ts.NotEmpty == "" {
|
||||
validator.Push(fmt.Errorf("NotEmpty must not be empty"))
|
||||
}
|
||||
|
||||
if ts.SetDefault == "" {
|
||||
ts.SetDefault = "xyz"
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator(t *testing.T) {
|
||||
validator := schema.NewValidator()
|
||||
|
||||
s := TestStruct{
|
||||
MustBe10: 5,
|
||||
NotEmpty: "",
|
||||
NestedPtr: &TestNestedStruct{},
|
||||
}
|
||||
|
||||
err := validator.Validate(&s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
errs := validator.Errors()
|
||||
assert.Equal(t, 4, len(errs))
|
||||
|
||||
assert.Equal(t, 2, len(errs["root"]))
|
||||
assert.ElementsMatch(t, []error{
|
||||
fmt.Errorf("MustBe10 must be 10"),
|
||||
fmt.Errorf("NotEmpty must not be empty")}, errs["root"])
|
||||
|
||||
assert.Equal(t, 1, len(errs["root.Nested"]))
|
||||
assert.ElementsMatch(t, []error{
|
||||
fmt.Errorf("MustBe5 must be 5")}, errs["root.Nested"])
|
||||
|
||||
assert.Equal(t, 1, len(errs["root.Nested2"]))
|
||||
assert.ElementsMatch(t, []error{
|
||||
fmt.Errorf("MustBe5 must be 5")}, errs["root.Nested2"])
|
||||
|
||||
assert.Equal(t, 1, len(errs["root.NestedPtr"]))
|
||||
assert.ElementsMatch(t, []error{
|
||||
fmt.Errorf("MustBe5 must be 5")}, errs["root.NestedPtr"])
|
||||
|
||||
assert.Equal(t, "xyz", s.SetDefault)
|
||||
}
|
64
configuration/validator/authentication.go
Normal file
64
configuration/validator/authentication.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
)
|
||||
|
||||
func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) {
|
||||
if configuration.Path == "" {
|
||||
validator.Push(errors.New("Please provide a `path` for the users database in `authentication_backend`"))
|
||||
}
|
||||
}
|
||||
|
||||
func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) {
|
||||
if configuration.URL == "" {
|
||||
validator.Push(errors.New("Please provide a URL to the LDAP server"))
|
||||
}
|
||||
|
||||
if configuration.User == "" {
|
||||
validator.Push(errors.New("Please provide a user name to connect to the LDAP server"))
|
||||
}
|
||||
|
||||
if configuration.Password == "" {
|
||||
validator.Push(errors.New("Please provide a password to connect to the LDAP server"))
|
||||
}
|
||||
|
||||
if configuration.BaseDN == "" {
|
||||
validator.Push(errors.New("Please provide a base DN to connect to the LDAP server"))
|
||||
}
|
||||
|
||||
if configuration.UsersFilter == "" {
|
||||
configuration.UsersFilter = "cn={0}"
|
||||
}
|
||||
|
||||
if configuration.GroupsFilter == "" {
|
||||
configuration.GroupsFilter = "member={dn}"
|
||||
}
|
||||
|
||||
if configuration.GroupNameAttribute == "" {
|
||||
configuration.GroupNameAttribute = "cn"
|
||||
}
|
||||
|
||||
if configuration.MailAttribute == "" {
|
||||
configuration.MailAttribute = "mail"
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAuthenticationBackend validates and update authentication backend configuration.
|
||||
func ValidateAuthenticationBackend(configuration *schema.AuthenticationBackendConfiguration, validator *schema.StructValidator) {
|
||||
if configuration.Ldap == nil && configuration.File == nil {
|
||||
validator.Push(errors.New("Please provide `ldap` or `file` object in `authentication_backend`"))
|
||||
}
|
||||
|
||||
if configuration.Ldap != nil && configuration.File != nil {
|
||||
validator.Push(errors.New("You cannot provide both `ldap` and `file` objects in `authentication_backend`"))
|
||||
}
|
||||
|
||||
if configuration.File != nil {
|
||||
validateFileAuthenticationBackend(configuration.File, validator)
|
||||
} else if configuration.Ldap != nil {
|
||||
validateLdapAuthenticationBackend(configuration.Ldap, validator)
|
||||
}
|
||||
}
|
124
configuration/validator/authentication_test.go
Normal file
124
configuration/validator/authentication_test.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestShouldRaiseErrorsWhenNoBackendProvided(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
backendConfig := schema.AuthenticationBackendConfiguration{}
|
||||
|
||||
ValidateAuthenticationBackend(&backendConfig, validator)
|
||||
|
||||
assert.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "Please provide `ldap` or `file` object in `authentication_backend`")
|
||||
}
|
||||
|
||||
type FileBasedAuthenticationBackend struct {
|
||||
suite.Suite
|
||||
configuration schema.AuthenticationBackendConfiguration
|
||||
validator *schema.StructValidator
|
||||
}
|
||||
|
||||
func (suite *FileBasedAuthenticationBackend) SetupTest() {
|
||||
suite.validator = schema.NewStructValidator()
|
||||
suite.configuration = schema.AuthenticationBackendConfiguration{}
|
||||
suite.configuration.File = &schema.FileAuthenticationBackendConfiguration{Path: "/a/path"}
|
||||
}
|
||||
|
||||
func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfiguration() {
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||
}
|
||||
|
||||
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvided() {
|
||||
suite.configuration.File.Path = ""
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 1)
|
||||
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a `path` for the users database in `authentication_backend`")
|
||||
}
|
||||
|
||||
func TestFileBasedAuthenticationBackend(t *testing.T) {
|
||||
suite.Run(t, new(FileBasedAuthenticationBackend))
|
||||
}
|
||||
|
||||
type LdapAuthenticationBackendSuite struct {
|
||||
suite.Suite
|
||||
configuration schema.AuthenticationBackendConfiguration
|
||||
validator *schema.StructValidator
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) SetupTest() {
|
||||
suite.validator = schema.NewStructValidator()
|
||||
suite.configuration = schema.AuthenticationBackendConfiguration{}
|
||||
suite.configuration.Ldap = &schema.LDAPAuthenticationBackendConfiguration{}
|
||||
suite.configuration.Ldap.URL = "ldap://ldap"
|
||||
suite.configuration.Ldap.User = "user"
|
||||
suite.configuration.Ldap.Password = "password"
|
||||
suite.configuration.Ldap.BaseDN = "base_dn"
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldValidateCompleteConfiguration() {
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() {
|
||||
suite.configuration.Ldap.URL = ""
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 1)
|
||||
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a URL to the LDAP server")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenUserNotProvided() {
|
||||
suite.configuration.Ldap.User = ""
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 1)
|
||||
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a user name to connect to the LDAP server")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenPasswordNotProvided() {
|
||||
suite.configuration.Ldap.Password = ""
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 1)
|
||||
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a password to connect to the LDAP server")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenBaseDNNotProvided() {
|
||||
suite.configuration.Ldap.BaseDN = ""
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 1)
|
||||
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a base DN to connect to the LDAP server")
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultUsersFilter() {
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||
assert.Equal(suite.T(), "cn={0}", suite.configuration.Ldap.UsersFilter)
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupsFilter() {
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||
assert.Equal(suite.T(), "member={dn}", suite.configuration.Ldap.GroupsFilter)
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttribute() {
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||
assert.Equal(suite.T(), "cn", suite.configuration.Ldap.GroupNameAttribute)
|
||||
}
|
||||
|
||||
func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() {
|
||||
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
|
||||
assert.Len(suite.T(), suite.validator.Errors(), 0)
|
||||
assert.Equal(suite.T(), "mail", suite.configuration.Ldap.MailAttribute)
|
||||
}
|
||||
|
||||
func TestLdapAuthenticationBackend(t *testing.T) {
|
||||
suite.Run(t, new(LdapAuthenticationBackendSuite))
|
||||
}
|
33
configuration/validator/configuration.go
Normal file
33
configuration/validator/configuration.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
)
|
||||
|
||||
var defaultPort = 8080
|
||||
var defaultLogsLevel = "info"
|
||||
|
||||
// Validate and adapt the configuration read from file.
|
||||
func Validate(configuration *schema.Configuration, validator *schema.StructValidator) {
|
||||
if configuration.Port == 0 {
|
||||
configuration.Port = defaultPort
|
||||
}
|
||||
|
||||
if configuration.LogsLevel == "" {
|
||||
configuration.LogsLevel = defaultLogsLevel
|
||||
}
|
||||
|
||||
if configuration.JWTSecret == "" {
|
||||
validator.Push(fmt.Errorf("Provide a JWT secret using `jwt_secret` key"))
|
||||
}
|
||||
|
||||
ValidateAuthenticationBackend(&configuration.AuthenticationBackend, validator)
|
||||
ValidateSession(&configuration.Session, validator)
|
||||
|
||||
if configuration.TOTP == nil {
|
||||
configuration.TOTP = &schema.TOTPConfiguration{}
|
||||
ValidateTOTP(configuration.TOTP, validator)
|
||||
}
|
||||
}
|
56
configuration/validator/configuration_test.go
Normal file
56
configuration/validator/configuration_test.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newDefaultConfig() schema.Configuration {
|
||||
config := schema.Configuration{}
|
||||
config.Port = 9090
|
||||
config.LogsLevel = "info"
|
||||
config.JWTSecret = "a_secret"
|
||||
config.AuthenticationBackend.File = new(schema.FileAuthenticationBackendConfiguration)
|
||||
config.AuthenticationBackend.File.Path = "/a/path"
|
||||
config.Session = schema.SessionConfiguration{
|
||||
Domain: "example.com",
|
||||
Name: "authelia_session",
|
||||
Secret: "secret",
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
func TestShouldNotUpdateConfig(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
|
||||
Validate(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
assert.Equal(t, 9090, config.Port)
|
||||
assert.Equal(t, "info", config.LogsLevel)
|
||||
}
|
||||
|
||||
func TestShouldValidateAndUpdatePort(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
config.Port = 0
|
||||
|
||||
Validate(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
assert.Equal(t, 8080, config.Port)
|
||||
}
|
||||
|
||||
func TestShouldValidateAndUpdateLogsLevel(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultConfig()
|
||||
config.LogsLevel = ""
|
||||
|
||||
Validate(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
assert.Equal(t, "info", config.LogsLevel)
|
||||
}
|
26
configuration/validator/session.go
Normal file
26
configuration/validator/session.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
)
|
||||
|
||||
// ValidateSession validates and update session configuration.
|
||||
func ValidateSession(configuration *schema.SessionConfiguration, validator *schema.StructValidator) {
|
||||
if configuration.Name == "" {
|
||||
configuration.Name = schema.DefaultSessionConfiguration.Name
|
||||
}
|
||||
|
||||
if configuration.Secret == "" {
|
||||
validator.Push(errors.New("Set secret of the session object"))
|
||||
}
|
||||
|
||||
if configuration.Expiration == 0 {
|
||||
configuration.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour
|
||||
}
|
||||
|
||||
if configuration.Domain == "" {
|
||||
validator.Push(errors.New("Set domain of the session object"))
|
||||
}
|
||||
}
|
47
configuration/validator/session_test.go
Normal file
47
configuration/validator/session_test.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func newDefaultSessionConfig() schema.SessionConfiguration {
|
||||
config := schema.SessionConfiguration{}
|
||||
config.Secret = "a_secret"
|
||||
config.Domain = "example.com"
|
||||
return config
|
||||
}
|
||||
|
||||
func TestShouldSetDefaultSessionName(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Errors(), 0)
|
||||
assert.Equal(t, "authelia_session", config.Name)
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenPasswordNotSet(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.Secret = ""
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "Set secret of the session object")
|
||||
}
|
||||
|
||||
func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
|
||||
validator := schema.NewStructValidator()
|
||||
config := newDefaultSessionConfig()
|
||||
config.Domain = ""
|
||||
|
||||
ValidateSession(&config, validator)
|
||||
|
||||
assert.Len(t, validator.Errors(), 1)
|
||||
assert.EqualError(t, validator.Errors()[0], "Set domain of the session object")
|
||||
}
|
14
configuration/validator/totp.go
Normal file
14
configuration/validator/totp.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
)
|
||||
|
||||
const defaultTOTPIssuer = "Authelia"
|
||||
|
||||
// ValidateTOTP validates and update TOTP configuration.
|
||||
func ValidateTOTP(configuration *schema.TOTPConfiguration, validator *schema.StructValidator) {
|
||||
if configuration.Issuer == "" {
|
||||
configuration.Issuer = defaultTOTPIssuer
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ Here is a commented example of configuration
|
|||
set $upstream_verify https://authelia.example.com/api/verify;
|
||||
set $upstream_endpoint http://nginx-backend;
|
||||
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
# Use HSTS, please beware of what you're doing if you set it.
|
||||
|
|
32
duo/duo.go
Normal file
32
duo/duo.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package duo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
|
||||
"github.com/duosecurity/duo_api_golang"
|
||||
)
|
||||
|
||||
// NewDuoAPI create duo API instance
|
||||
func NewDuoAPI(duoAPI *duoapi.DuoApi) *APIImpl {
|
||||
api := new(APIImpl)
|
||||
api.DuoApi = duoAPI
|
||||
return api
|
||||
}
|
||||
|
||||
// Call call to the DuoAPI
|
||||
func (d *APIImpl) Call(values url.Values) (*Response, error) {
|
||||
_, responseBytes, err := d.DuoApi.SignedCall("POST", "/auth/v2/auth", values)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var response Response
|
||||
err = json.Unmarshal(responseBytes, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &response, nil
|
||||
}
|
24
duo/types.go
Normal file
24
duo/types.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package duo
|
||||
|
||||
import "net/url"
|
||||
import "github.com/duosecurity/duo_api_golang"
|
||||
|
||||
// API interface wrapping duo api library for testing purpose
|
||||
type API interface {
|
||||
Call(values url.Values) (*Response, error)
|
||||
}
|
||||
|
||||
// APIImpl implementation of DuoAPI interface
|
||||
type APIImpl struct {
|
||||
*duoapi.DuoApi
|
||||
}
|
||||
|
||||
// Response response coming from Duo API
|
||||
type Response struct {
|
||||
Response struct {
|
||||
Result string `json:"result"`
|
||||
Status string `json:"status"`
|
||||
StatusMessage string `json:"status_msg"`
|
||||
} `json:"response"`
|
||||
Stat string `json:"stat"`
|
||||
}
|
|
@ -15,7 +15,7 @@ http {
|
|||
|
||||
resolver 127.0.0.11 ipv6=off;
|
||||
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
|
19
example/compose/nginx/kubernetes/ssl/server.cert
Normal file
19
example/compose/nginx/kubernetes/ssl/server.cert
Normal file
|
@ -0,0 +1,19 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDEzCCAfugAwIBAgIUJZXxXExVQPJhc8TnlD+uAAYHlvwwDQYJKoZIhvcNAQEL
|
||||
BQAwGDEWMBQGA1UEAwwNKi5leGFtcGxlLmNvbTAgFw0xOTA5MjYyMDAwMTBaGA8y
|
||||
MTE5MDkwMjIwMDAxMFowGDEWMBQGA1UEAwwNKi5leGFtcGxlLmNvbTCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3DFTAdrxG6iOj5UjSeB5lMjMQQyeYm
|
||||
OxUvswwwBzmQYPUt0inAJ9QmXJ8i9Fbye8HHYUeqE5zsEfeHir81MiWfhi9oUzJt
|
||||
u3bmxGLDXYaApejd18hBKITX6MYogmK2lWrl/F9zPYxc2xM/fqWnGg2xwdrMmida
|
||||
hZjDUfh0rtoz8zqOzJaiiDoFMwNO+NTGmDbeOwBFYOF1OTkS3aJWwJCLZmINUG8h
|
||||
Z3YPR+SL8CpGGl0xhJYAwXD1AtMlYwAteTILqrqvo2XkGsvuj0mx0w/D0DDpC48g
|
||||
oSNsRIVTW3Ql3uu+kXDFtkf4I63Ctt85rZk1kX3QtYmS0pRzvmyY/b0CAwEAAaNT
|
||||
MFEwHQYDVR0OBBYEFMTozK79Kp813+8TstjXRFw1MTE5MB8GA1UdIwQYMBaAFMTo
|
||||
zK79Kp813+8TstjXRFw1MTE5MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
|
||||
BQADggEBALf1bJf3qF3m54+q98E6lSE+34yi/rVdzB9reAW1QzvvqdJRtsfjt39R
|
||||
SznsbmrvCfK4SLyOj9Uhd8Z6bASPPNsUux1XAGN4AqaGmlYI8b7j3LhKCdRBZQ0I
|
||||
zWgPhocyWwp5VkFe68zR06NHme/2B6eBRFsdd/69DIOv9YnEGUHk3A/9v1zvolt9
|
||||
krW57Oz63zWGYXmtPPTD8of/Ya6NKqwonVx1MUQ5QzqH3WySYhRsIYqwUEXm9jt5
|
||||
GEM3Nx0phEltaOLXa71nqS/Rhg/5Kod0cFaNoSKb6N93I8bqKKTK0m5wMJ5Fisrm
|
||||
Pw5+AIar7RT5gHU2DD2/OTb9bXXww8I=
|
||||
-----END CERTIFICATE-----
|
|
@ -1,13 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICATCCAWoCCQCvH2RvyOshNzANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
|
||||
VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
|
||||
cyBQdHkgTHRkMB4XDTE3MDExNzIzMTc0M1oXDTE4MDExNzIzMTc0M1owRTELMAkG
|
||||
A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
|
||||
IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzZaE
|
||||
4XE1QyFNbrHBHRhSA53anAsJ5mBeG7Om6SdQcZAYahlDWEbtdoY4hy0gPNGcITcW
|
||||
eE+WA+PvNRr7PczKEhneIyUUgV+nrz010fM5JnECPxLTe1oFzl4U8dyYiBpTziNz
|
||||
hiUfq733PRYjcd9BQtcKcN4LdmQvjUHnnQ73TysCAwEAATANBgkqhkiG9w0BAQsF
|
||||
AAOBgQAUFICtbuqXgL4HBRAg7yGbwokoH8Ar1QKZGe+F2WTR8vaDLOYUL7VsltLE
|
||||
EJIGrcfs31nItHOBcLJuflrS8y0CQqes5puRw33LL2usSvO8z2q7JhCx+DSBi6yN
|
||||
RbhcrGOllIdjsrbmd/zAMBVTUyxSisq3Nmk1cZayDvKg+GSAEA==
|
||||
-----END CERTIFICATE-----
|
|
@ -1,11 +0,0 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
|
||||
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB
|
||||
AQUAA4GNADCBiQKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhq
|
||||
GUNYRu12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/
|
||||
EtN7WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwID
|
||||
AQABoAAwDQYJKoZIhvcNAQELBQADgYEAmCX60kspIw1Zfb79AQOarFW5Q2K2h5Vx
|
||||
/cRbDyHlKtbmG77EtICccULyqf76B1gNRw5Zq3lSotSUcLzsWcdesXCFDC7k87Qf
|
||||
mpQKPj6GdTYJvdWf8aDwt32tAqWuBIRoAbdx5WbFPPWVfDcm7zDJefBrhNUDH0Qd
|
||||
vcnxjvPMmOM=
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -1,15 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhqGUNY
|
||||
Ru12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/EtN7
|
||||
WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwIDAQAB
|
||||
AoGBAIwGcfkO30UawJ+daDeF4g5ejI/toM+NYWuiwBNbWJoQl+Bj1o+gt4obvxKq
|
||||
tKNX7OxelepZ4oZB0CIuf2LHQfU6cVGdu//or7nfS2FLBYStopZyL6KorZbkqsj1
|
||||
ikQN4GosJQqaYkexnwjItMFaHaRRX6YnIXp42Jl1glitO3+5AkEA+thn/vwFo24I
|
||||
fC+7ORpmLi+BVAkTuhMm+C6TIV6s64B+A5oQ82OBCYK9YCOWmS6JHHFDrxJla+3M
|
||||
2U9KXky63wJBANHQCFCirfuT6esSjbqpCeqtmZG5LWHtL12V9DF7yjHPjmHL9uRu
|
||||
e9W+Uz33IJbqd82gtZ/ARfpYEjD0JEieQTUCQFo872xzDTQ1qSfDo/5u2MNUo5mv
|
||||
ikEuEp7FYnhmrp4poyt4iRCFgy4Ask+bfdmtO/XXaRnZ7FJfQYoLVB2ITNECQQCN
|
||||
gOiauZztl4yj5heAVJFDnWF9To61BOp1C7VtyjdL8NfuTUluNrV+KqapnAp2vhue
|
||||
q0zTOTH47X0XVxFBiLohAkBuQzPey5I3Ui8inE4sDt/fqX8r/GMhBTxIb9KlV/H6
|
||||
jKZNs/83n5/ohaX36er8svW9PB4pcqENZ+kBpvDtKVwS
|
||||
MIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S
|
||||
KcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X
|
||||
yEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M
|
||||
lqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE
|
||||
lgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R
|
||||
cMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn
|
||||
V3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv
|
||||
B7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd
|
||||
zV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036
|
||||
UxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1
|
||||
/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI
|
||||
F4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd
|
||||
7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs
|
||||
hcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA
|
||||
06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh
|
||||
IlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75
|
||||
HmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/
|
||||
rW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE
|
||||
Zrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b
|
||||
bx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq
|
||||
0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS
|
||||
qfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2
|
||||
qSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L
|
||||
zqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2
|
||||
HEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
|
|
@ -18,7 +18,7 @@ http {
|
|||
resolver 127.0.0.11 ipv6=off;
|
||||
set $backend_endpoint <%= authelia_backend %>;
|
||||
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
@ -26,7 +26,7 @@ http {
|
|||
|
||||
# Serves the portal application.
|
||||
location / {
|
||||
proxy_pass $backend_endpoint/index.html;
|
||||
proxy_pass $backend_endpoint;
|
||||
}
|
||||
|
||||
location /static {
|
||||
|
@ -62,7 +62,7 @@ http {
|
|||
set $frontend_endpoint http://192.168.240.1:3000;
|
||||
set $backend_endpoint <%= authelia_backend %>;
|
||||
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
@ -108,7 +108,7 @@ http {
|
|||
resolver 127.0.0.11 ipv6=off;
|
||||
set $upstream_endpoint http://nginx-backend;
|
||||
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
@ -135,7 +135,7 @@ http {
|
|||
set $upstream_endpoint http://nginx-backend;
|
||||
set $upstream_headers http://httpbin:8000/headers;
|
||||
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
@ -179,7 +179,7 @@ http {
|
|||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
# Provide either X-Original-URL and X-Forwarded-Proto or
|
||||
# X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri or both.
|
||||
# X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-URI or both.
|
||||
# Those headers will be used by Authelia to deduce the target url of the user.
|
||||
#
|
||||
# X-Forwarded-Proto is mandatory since Authelia uses the "trust proxy" option.
|
||||
|
@ -188,7 +188,7 @@ http {
|
|||
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Uri $request_uri;
|
||||
proxy_set_header X-Forwarded-URI $request_uri;
|
||||
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
|
@ -227,7 +227,7 @@ http {
|
|||
resolver 127.0.0.11 ipv6=off;
|
||||
set $upstream_endpoint http://smtp:1080;
|
||||
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
@ -247,7 +247,7 @@ http {
|
|||
resolver 127.0.0.11 ipv6=off;
|
||||
set $upstream_endpoint http://duo-api:3000;
|
||||
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
|
@ -264,7 +264,7 @@ http {
|
|||
listen 8080 ssl;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/ssl/server.crt;
|
||||
ssl_certificate /etc/ssl/server.cert;
|
||||
ssl_certificate_key /etc/ssl/server.key;
|
||||
|
||||
return 301 https://home.example.com:8080/;
|
||||
|
|
19
example/compose/nginx/portal/ssl/server.cert
Normal file
19
example/compose/nginx/portal/ssl/server.cert
Normal file
|
@ -0,0 +1,19 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDEzCCAfugAwIBAgIUJZXxXExVQPJhc8TnlD+uAAYHlvwwDQYJKoZIhvcNAQEL
|
||||
BQAwGDEWMBQGA1UEAwwNKi5leGFtcGxlLmNvbTAgFw0xOTA5MjYyMDAwMTBaGA8y
|
||||
MTE5MDkwMjIwMDAxMFowGDEWMBQGA1UEAwwNKi5leGFtcGxlLmNvbTCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3DFTAdrxG6iOj5UjSeB5lMjMQQyeYm
|
||||
OxUvswwwBzmQYPUt0inAJ9QmXJ8i9Fbye8HHYUeqE5zsEfeHir81MiWfhi9oUzJt
|
||||
u3bmxGLDXYaApejd18hBKITX6MYogmK2lWrl/F9zPYxc2xM/fqWnGg2xwdrMmida
|
||||
hZjDUfh0rtoz8zqOzJaiiDoFMwNO+NTGmDbeOwBFYOF1OTkS3aJWwJCLZmINUG8h
|
||||
Z3YPR+SL8CpGGl0xhJYAwXD1AtMlYwAteTILqrqvo2XkGsvuj0mx0w/D0DDpC48g
|
||||
oSNsRIVTW3Ql3uu+kXDFtkf4I63Ctt85rZk1kX3QtYmS0pRzvmyY/b0CAwEAAaNT
|
||||
MFEwHQYDVR0OBBYEFMTozK79Kp813+8TstjXRFw1MTE5MB8GA1UdIwQYMBaAFMTo
|
||||
zK79Kp813+8TstjXRFw1MTE5MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
|
||||
BQADggEBALf1bJf3qF3m54+q98E6lSE+34yi/rVdzB9reAW1QzvvqdJRtsfjt39R
|
||||
SznsbmrvCfK4SLyOj9Uhd8Z6bASPPNsUux1XAGN4AqaGmlYI8b7j3LhKCdRBZQ0I
|
||||
zWgPhocyWwp5VkFe68zR06NHme/2B6eBRFsdd/69DIOv9YnEGUHk3A/9v1zvolt9
|
||||
krW57Oz63zWGYXmtPPTD8of/Ya6NKqwonVx1MUQ5QzqH3WySYhRsIYqwUEXm9jt5
|
||||
GEM3Nx0phEltaOLXa71nqS/Rhg/5Kod0cFaNoSKb6N93I8bqKKTK0m5wMJ5Fisrm
|
||||
Pw5+AIar7RT5gHU2DD2/OTb9bXXww8I=
|
||||
-----END CERTIFICATE-----
|
|
@ -1,13 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICATCCAWoCCQCvH2RvyOshNzANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
|
||||
VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
|
||||
cyBQdHkgTHRkMB4XDTE3MDExNzIzMTc0M1oXDTE4MDExNzIzMTc0M1owRTELMAkG
|
||||
A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
|
||||
IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzZaE
|
||||
4XE1QyFNbrHBHRhSA53anAsJ5mBeG7Om6SdQcZAYahlDWEbtdoY4hy0gPNGcITcW
|
||||
eE+WA+PvNRr7PczKEhneIyUUgV+nrz010fM5JnECPxLTe1oFzl4U8dyYiBpTziNz
|
||||
hiUfq733PRYjcd9BQtcKcN4LdmQvjUHnnQ73TysCAwEAATANBgkqhkiG9w0BAQsF
|
||||
AAOBgQAUFICtbuqXgL4HBRAg7yGbwokoH8Ar1QKZGe+F2WTR8vaDLOYUL7VsltLE
|
||||
EJIGrcfs31nItHOBcLJuflrS8y0CQqes5puRw33LL2usSvO8z2q7JhCx+DSBi6yN
|
||||
RbhcrGOllIdjsrbmd/zAMBVTUyxSisq3Nmk1cZayDvKg+GSAEA==
|
||||
-----END CERTIFICATE-----
|
|
@ -1,11 +0,0 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
|
||||
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB
|
||||
AQUAA4GNADCBiQKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhq
|
||||
GUNYRu12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/
|
||||
EtN7WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwID
|
||||
AQABoAAwDQYJKoZIhvcNAQELBQADgYEAmCX60kspIw1Zfb79AQOarFW5Q2K2h5Vx
|
||||
/cRbDyHlKtbmG77EtICccULyqf76B1gNRw5Zq3lSotSUcLzsWcdesXCFDC7k87Qf
|
||||
mpQKPj6GdTYJvdWf8aDwt32tAqWuBIRoAbdx5WbFPPWVfDcm7zDJefBrhNUDH0Qd
|
||||
vcnxjvPMmOM=
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -1,15 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhqGUNY
|
||||
Ru12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/EtN7
|
||||
WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwIDAQAB
|
||||
AoGBAIwGcfkO30UawJ+daDeF4g5ejI/toM+NYWuiwBNbWJoQl+Bj1o+gt4obvxKq
|
||||
tKNX7OxelepZ4oZB0CIuf2LHQfU6cVGdu//or7nfS2FLBYStopZyL6KorZbkqsj1
|
||||
ikQN4GosJQqaYkexnwjItMFaHaRRX6YnIXp42Jl1glitO3+5AkEA+thn/vwFo24I
|
||||
fC+7ORpmLi+BVAkTuhMm+C6TIV6s64B+A5oQ82OBCYK9YCOWmS6JHHFDrxJla+3M
|
||||
2U9KXky63wJBANHQCFCirfuT6esSjbqpCeqtmZG5LWHtL12V9DF7yjHPjmHL9uRu
|
||||
e9W+Uz33IJbqd82gtZ/ARfpYEjD0JEieQTUCQFo872xzDTQ1qSfDo/5u2MNUo5mv
|
||||
ikEuEp7FYnhmrp4poyt4iRCFgy4Ask+bfdmtO/XXaRnZ7FJfQYoLVB2ITNECQQCN
|
||||
gOiauZztl4yj5heAVJFDnWF9To61BOp1C7VtyjdL8NfuTUluNrV+KqapnAp2vhue
|
||||
q0zTOTH47X0XVxFBiLohAkBuQzPey5I3Ui8inE4sDt/fqX8r/GMhBTxIb9KlV/H6
|
||||
jKZNs/83n5/ohaX36er8svW9PB4pcqENZ+kBpvDtKVwS
|
||||
MIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S
|
||||
KcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X
|
||||
yEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M
|
||||
lqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE
|
||||
lgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R
|
||||
cMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn
|
||||
V3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv
|
||||
B7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd
|
||||
zV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036
|
||||
UxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1
|
||||
/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI
|
||||
F4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd
|
||||
7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs
|
||||
hcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA
|
||||
06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh
|
||||
IlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75
|
||||
HmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/
|
||||
rW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE
|
||||
Zrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b
|
||||
bx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq
|
||||
0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS
|
||||
qfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2
|
||||
qSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L
|
||||
zqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2
|
||||
HEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
|
|
19
example/kube/apps/ssl/server.cert
Normal file
19
example/kube/apps/ssl/server.cert
Normal file
|
@ -0,0 +1,19 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDEzCCAfugAwIBAgIUJZXxXExVQPJhc8TnlD+uAAYHlvwwDQYJKoZIhvcNAQEL
|
||||
BQAwGDEWMBQGA1UEAwwNKi5leGFtcGxlLmNvbTAgFw0xOTA5MjYyMDAwMTBaGA8y
|
||||
MTE5MDkwMjIwMDAxMFowGDEWMBQGA1UEAwwNKi5leGFtcGxlLmNvbTCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3DFTAdrxG6iOj5UjSeB5lMjMQQyeYm
|
||||
OxUvswwwBzmQYPUt0inAJ9QmXJ8i9Fbye8HHYUeqE5zsEfeHir81MiWfhi9oUzJt
|
||||
u3bmxGLDXYaApejd18hBKITX6MYogmK2lWrl/F9zPYxc2xM/fqWnGg2xwdrMmida
|
||||
hZjDUfh0rtoz8zqOzJaiiDoFMwNO+NTGmDbeOwBFYOF1OTkS3aJWwJCLZmINUG8h
|
||||
Z3YPR+SL8CpGGl0xhJYAwXD1AtMlYwAteTILqrqvo2XkGsvuj0mx0w/D0DDpC48g
|
||||
oSNsRIVTW3Ql3uu+kXDFtkf4I63Ctt85rZk1kX3QtYmS0pRzvmyY/b0CAwEAAaNT
|
||||
MFEwHQYDVR0OBBYEFMTozK79Kp813+8TstjXRFw1MTE5MB8GA1UdIwQYMBaAFMTo
|
||||
zK79Kp813+8TstjXRFw1MTE5MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
|
||||
BQADggEBALf1bJf3qF3m54+q98E6lSE+34yi/rVdzB9reAW1QzvvqdJRtsfjt39R
|
||||
SznsbmrvCfK4SLyOj9Uhd8Z6bASPPNsUux1XAGN4AqaGmlYI8b7j3LhKCdRBZQ0I
|
||||
zWgPhocyWwp5VkFe68zR06NHme/2B6eBRFsdd/69DIOv9YnEGUHk3A/9v1zvolt9
|
||||
krW57Oz63zWGYXmtPPTD8of/Ya6NKqwonVx1MUQ5QzqH3WySYhRsIYqwUEXm9jt5
|
||||
GEM3Nx0phEltaOLXa71nqS/Rhg/5Kod0cFaNoSKb6N93I8bqKKTK0m5wMJ5Fisrm
|
||||
Pw5+AIar7RT5gHU2DD2/OTb9bXXww8I=
|
||||
-----END CERTIFICATE-----
|
27
example/kube/apps/ssl/server.key
Normal file
27
example/kube/apps/ssl/server.key
Normal file
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S
|
||||
KcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X
|
||||
yEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M
|
||||
lqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE
|
||||
lgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R
|
||||
cMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn
|
||||
V3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv
|
||||
B7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd
|
||||
zV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036
|
||||
UxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1
|
||||
/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI
|
||||
F4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd
|
||||
7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs
|
||||
hcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA
|
||||
06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh
|
||||
IlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75
|
||||
HmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/
|
||||
rW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE
|
||||
Zrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b
|
||||
bx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq
|
||||
0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS
|
||||
qfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2
|
||||
qSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L
|
||||
zqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2
|
||||
HEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -1,13 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICATCCAWoCCQCvH2RvyOshNzANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
|
||||
VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
|
||||
cyBQdHkgTHRkMB4XDTE3MDExNzIzMTc0M1oXDTE4MDExNzIzMTc0M1owRTELMAkG
|
||||
A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
|
||||
IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAzZaE
|
||||
4XE1QyFNbrHBHRhSA53anAsJ5mBeG7Om6SdQcZAYahlDWEbtdoY4hy0gPNGcITcW
|
||||
eE+WA+PvNRr7PczKEhneIyUUgV+nrz010fM5JnECPxLTe1oFzl4U8dyYiBpTziNz
|
||||
hiUfq733PRYjcd9BQtcKcN4LdmQvjUHnnQ73TysCAwEAATANBgkqhkiG9w0BAQsF
|
||||
AAOBgQAUFICtbuqXgL4HBRAg7yGbwokoH8Ar1QKZGe+F2WTR8vaDLOYUL7VsltLE
|
||||
EJIGrcfs31nItHOBcLJuflrS8y0CQqes5puRw33LL2usSvO8z2q7JhCx+DSBi6yN
|
||||
RbhcrGOllIdjsrbmd/zAMBVTUyxSisq3Nmk1cZayDvKg+GSAEA==
|
||||
-----END CERTIFICATE-----
|
|
@ -1,15 +0,0 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICXQIBAAKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhqGUNY
|
||||
Ru12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/EtN7
|
||||
WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwIDAQAB
|
||||
AoGBAIwGcfkO30UawJ+daDeF4g5ejI/toM+NYWuiwBNbWJoQl+Bj1o+gt4obvxKq
|
||||
tKNX7OxelepZ4oZB0CIuf2LHQfU6cVGdu//or7nfS2FLBYStopZyL6KorZbkqsj1
|
||||
ikQN4GosJQqaYkexnwjItMFaHaRRX6YnIXp42Jl1glitO3+5AkEA+thn/vwFo24I
|
||||
fC+7ORpmLi+BVAkTuhMm+C6TIV6s64B+A5oQ82OBCYK9YCOWmS6JHHFDrxJla+3M
|
||||
2U9KXky63wJBANHQCFCirfuT6esSjbqpCeqtmZG5LWHtL12V9DF7yjHPjmHL9uRu
|
||||
e9W+Uz33IJbqd82gtZ/ARfpYEjD0JEieQTUCQFo872xzDTQ1qSfDo/5u2MNUo5mv
|
||||
ikEuEp7FYnhmrp4poyt4iRCFgy4Ask+bfdmtO/XXaRnZ7FJfQYoLVB2ITNECQQCN
|
||||
gOiauZztl4yj5heAVJFDnWF9To61BOp1C7VtyjdL8NfuTUluNrV+KqapnAp2vhue
|
||||
q0zTOTH47X0XVxFBiLohAkBuQzPey5I3Ui8inE4sDt/fqX8r/GMhBTxIb9KlV/H6
|
||||
jKZNs/83n5/ohaX36er8svW9PB4pcqENZ+kBpvDtKVwS
|
||||
-----END RSA PRIVATE KEY-----
|
|
@ -10,6 +10,8 @@ port: 80
|
|||
# Level of verbosity for logs
|
||||
logs_level: debug
|
||||
|
||||
jwt_secret: an_unsecure_secret
|
||||
|
||||
# Default redirection URL
|
||||
#
|
||||
# If user tries to authenticate without any referer, Authelia
|
||||
|
@ -35,7 +37,7 @@ authentication_backend:
|
|||
# production.
|
||||
ldap:
|
||||
# The url of the ldap server
|
||||
url: ldap://ldap-service
|
||||
url: ldap-service:389
|
||||
|
||||
# The base dn for every entries
|
||||
base_dn: dc=example,dc=com
|
||||
|
@ -46,7 +48,7 @@ authentication_backend:
|
|||
# The users filter used to find the user DN
|
||||
# {0} is a matcher replaced by username.
|
||||
# 'cn={0}' by default.
|
||||
users_filter: cn={0}
|
||||
users_filter: (cn={0})
|
||||
|
||||
# An additional dn to define the scope of groups
|
||||
additional_groups_dn: ou=groups
|
||||
|
@ -195,20 +197,9 @@ notifier:
|
|||
# For testing purpose, notifications can be sent in a file
|
||||
# filesystem:
|
||||
# filename: /tmp/authelia/notification.txt
|
||||
|
||||
# Use your email account to send the notifications. You can use an app password.
|
||||
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/
|
||||
# email:
|
||||
# username: authelia@gmail.com
|
||||
# password: password
|
||||
# sender: authelia@example.com
|
||||
# service: gmail
|
||||
|
||||
# Use a SMTP server for sending notifications
|
||||
smtp:
|
||||
username: test
|
||||
password: password
|
||||
secure: false
|
||||
host: 'mailcatcher-service'
|
||||
port: 1025
|
||||
sender: admin@example.com
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
start_apps() {
|
||||
# Create TLS certificate and key for HTTPS termination
|
||||
kubectl create secret generic test-app-tls --namespace=authelia --from-file=apps/ssl/tls.key --from-file=apps/ssl/tls.crt
|
||||
kubectl create secret generic test-app-tls --namespace=authelia --from-file=apps/ssl/server.key --from-file=apps/ssl/server.cert
|
||||
|
||||
# Spawn the applications
|
||||
kubectl apply -f apps
|
||||
|
@ -13,7 +13,11 @@ start_ingress_controller() {
|
|||
}
|
||||
|
||||
start_dashboard() {
|
||||
kubectl apply -f dashboard
|
||||
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml
|
||||
kubectl apply -f dashboard.yml
|
||||
|
||||
echo "Bearer token for UI user."
|
||||
kubectl -n kubernetes-dashboard describe secret $(kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print $1}')
|
||||
}
|
||||
|
||||
# Spawn Redis and Mongo as backend for Authelia
|
||||
|
@ -28,6 +32,7 @@ start_mail() {
|
|||
}
|
||||
|
||||
start_ldap() {
|
||||
kubectl create configmap ldap-config --namespace=authelia --from-file=ldap/base.ldif --from-file=ldap/access.rules
|
||||
kubectl apply -f ldap
|
||||
}
|
||||
|
||||
|
|
20
example/kube/dashboard.yml
Normal file
20
example/kube/dashboard.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: admin-user
|
||||
namespace: kubernetes-dashboard
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: admin-user
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: admin-user
|
||||
namespace: kubernetes-dashboard
|
|
@ -1,179 +0,0 @@
|
|||
# Copyright 2017 The Kubernetes Authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# ------------------- Dashboard Secret ------------------- #
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard-certs
|
||||
namespace: kube-system
|
||||
type: Opaque
|
||||
|
||||
---
|
||||
# ------------------- Dashboard Service Account ------------------- #
|
||||
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard
|
||||
namespace: kube-system
|
||||
|
||||
---
|
||||
# ------------------- Dashboard Role & Role Binding ------------------- #
|
||||
|
||||
kind: Role
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: kubernetes-dashboard-minimal
|
||||
namespace: kube-system
|
||||
rules:
|
||||
# Allow Dashboard to create 'kubernetes-dashboard-key-holder' secret.
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["create"]
|
||||
# Allow Dashboard to create 'kubernetes-dashboard-settings' config map.
|
||||
- apiGroups: [""]
|
||||
resources: ["configmaps"]
|
||||
verbs: ["create"]
|
||||
# Allow Dashboard to get, update and delete Dashboard exclusive secrets.
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
resourceNames: ["kubernetes-dashboard-key-holder", "kubernetes-dashboard-certs"]
|
||||
verbs: ["get", "update", "delete"]
|
||||
# Allow Dashboard to get and update 'kubernetes-dashboard-settings' config map.
|
||||
- apiGroups: [""]
|
||||
resources: ["configmaps"]
|
||||
resourceNames: ["kubernetes-dashboard-settings"]
|
||||
verbs: ["get", "update"]
|
||||
# Allow Dashboard to get metrics from heapster.
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
resourceNames: ["heapster"]
|
||||
verbs: ["proxy"]
|
||||
- apiGroups: [""]
|
||||
resources: ["services/proxy"]
|
||||
resourceNames: ["heapster", "http:heapster:", "https:heapster:"]
|
||||
verbs: ["get"]
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: kubernetes-dashboard-minimal
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: kubernetes-dashboard-minimal
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: kubernetes-dashboard
|
||||
namespace: kube-system
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: kubernetes-dashboard
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: kubernetes-dashboard
|
||||
namespace: kube-system
|
||||
|
||||
---
|
||||
# ------------------- Dashboard Deployment ------------------- #
|
||||
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard
|
||||
namespace: kube-system
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 10
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
spec:
|
||||
containers:
|
||||
- name: kubernetes-dashboard
|
||||
image: k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.1
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
protocol: TCP
|
||||
args:
|
||||
- --auto-generate-certificates
|
||||
- --enable-skip-login
|
||||
# Uncomment the following line to manually specify Kubernetes API server Host
|
||||
# If not specified, Dashboard will attempt to auto discover the API server and connect
|
||||
# to it. Uncomment only if the default does not work.
|
||||
# - --apiserver-host=http://my-address:port
|
||||
volumeMounts:
|
||||
- name: kubernetes-dashboard-certs
|
||||
mountPath: /certs
|
||||
# Create on-disk volume to store exec logs
|
||||
- mountPath: /tmp
|
||||
name: tmp-volume
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
scheme: HTTPS
|
||||
path: /
|
||||
port: 8443
|
||||
initialDelaySeconds: 30
|
||||
timeoutSeconds: 30
|
||||
volumes:
|
||||
- name: kubernetes-dashboard-certs
|
||||
secret:
|
||||
secretName: kubernetes-dashboard-certs
|
||||
- name: tmp-volume
|
||||
emptyDir: {}
|
||||
serviceAccountName: kubernetes-dashboard
|
||||
# Comment the following tolerations if Dashboard must not be deployed on master
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/master
|
||||
effect: NoSchedule
|
||||
|
||||
---
|
||||
# ------------------- Dashboard Service ------------------- #
|
||||
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
labels:
|
||||
k8s-app: kubernetes-dashboard
|
||||
name: kubernetes-dashboard
|
||||
namespace: kube-system
|
||||
spec:
|
||||
ports:
|
||||
- port: 443
|
||||
targetPort: 8443
|
||||
selector:
|
||||
k8s-app: kubernetes-dashboard
|
|
@ -1,12 +0,0 @@
|
|||
FROM clems4ever/openldap
|
||||
|
||||
ENV SLAPD_ORGANISATION=MyCompany
|
||||
ENV SLAPD_DOMAIN=example.com
|
||||
ENV SLAPD_PASSWORD=password
|
||||
ENV SLAPD_CONFIG_PASSWORD=password
|
||||
ENV SLAPD_ADDITIONAL_MODULES=memberof
|
||||
ENV SLAPD_ADDITIONAL_SCHEMAS=openldap
|
||||
ENV SLAPD_FORCE_RECONFIGURE=true
|
||||
|
||||
ADD base.ldif /etc/ldap.dist/prepopulate/base.ldif
|
||||
ADD access.rules /etc/ldap.dist/prepopulate/access.rules
|
7
example/kube/ldap/access.rules
Normal file
7
example/kube/ldap/access.rules
Normal file
|
@ -0,0 +1,7 @@
|
|||
olcAccess: {0}to attrs=userPassword,shadowLastChange by self write by anonymou
|
||||
s auth by * none
|
||||
# olcAccess: {1}to dn.base="" by * read
|
||||
# olcAccess: {2}to * by * read
|
||||
|
||||
olcPasswordHash: {CRYPT}
|
||||
olcPasswordCryptSaltFormat: $6$rounds=50000$%.16s
|
62
example/kube/ldap/base.ldif
Normal file
62
example/kube/ldap/base.ldif
Normal file
|
@ -0,0 +1,62 @@
|
|||
dn: ou=groups,dc=example,dc=com
|
||||
objectclass: organizationalUnit
|
||||
objectclass: top
|
||||
ou: groups
|
||||
|
||||
dn: ou=users,dc=example,dc=com
|
||||
objectclass: organizationalUnit
|
||||
objectclass: top
|
||||
ou: users
|
||||
|
||||
dn: cn=dev,ou=groups,dc=example,dc=com
|
||||
cn: dev
|
||||
member: cn=john,ou=users,dc=example,dc=com
|
||||
member: cn=bob,ou=users,dc=example,dc=com
|
||||
objectclass: groupOfNames
|
||||
objectclass: top
|
||||
|
||||
dn: cn=admin,ou=groups,dc=example,dc=com
|
||||
cn: admin
|
||||
member: cn=john,ou=users,dc=example,dc=com
|
||||
objectclass: groupOfNames
|
||||
objectclass: top
|
||||
|
||||
dn: cn=john,ou=users,dc=example,dc=com
|
||||
cn: john
|
||||
objectclass: inetOrgPerson
|
||||
objectclass: top
|
||||
mail: john.doe@authelia.com
|
||||
sn: John Doe
|
||||
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||
|
||||
dn: cn=harry,ou=users,dc=example,dc=com
|
||||
cn: harry
|
||||
objectclass: inetOrgPerson
|
||||
objectclass: top
|
||||
mail: harry.potter@authelia.com
|
||||
sn: Harry Potter
|
||||
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||
|
||||
dn: cn=bob,ou=users,dc=example,dc=com
|
||||
cn: bob
|
||||
objectclass: inetOrgPerson
|
||||
objectclass: top
|
||||
mail: bob.dylan@authelia.com
|
||||
sn: Bob Dylan
|
||||
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||
|
||||
dn: cn=james,ou=users,dc=example,dc=com
|
||||
cn: james
|
||||
objectclass: inetOrgPerson
|
||||
objectclass: top
|
||||
mail: james.dean@authelia.com
|
||||
sn: James Dean
|
||||
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
||||
|
||||
dn: cn=blackhat,ou=users,dc=example,dc=com
|
||||
cn: blackhat
|
||||
objectclass: inetOrgPerson
|
||||
objectclass: top
|
||||
mail: billy.blackhat@authelia.com
|
||||
sn: Billy BlackHat
|
||||
userpassword: {CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/
|
|
@ -21,3 +21,30 @@ spec:
|
|||
image: clems4ever/authelia-test-ldap
|
||||
ports:
|
||||
- containerPort: 389
|
||||
env:
|
||||
- name: SLAPD_ORGANISATION
|
||||
value: MyCompany
|
||||
- name: SLAPD_DOMAIN
|
||||
value: example.com
|
||||
- name: SLAPD_PASSWORD
|
||||
value: password
|
||||
- name: SLAPD_CONFIG_PASSWORD
|
||||
value: password
|
||||
- name: SLAPD_ADDITIONAL_MODULES
|
||||
value: memberof
|
||||
- name: SLAPD_ADDITIONAL_SCHEMAS
|
||||
value: openldap
|
||||
- name: SLAPD_FORCE_RECONFIGURE
|
||||
value: "true"
|
||||
volumeMounts:
|
||||
- name: config-volume
|
||||
mountPath: /etc/ldap.dist/prepopulate
|
||||
volumes:
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: ldap-config
|
||||
items:
|
||||
- key: base.ldif
|
||||
path: base.ldif
|
||||
- key: access.rules
|
||||
path: access.rules
|
||||
|
|
36
handlers/const.go
Normal file
36
handlers/const.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package handlers
|
||||
|
||||
// TOTPRegistrationAction is the string representation of the action for which the token has been produced.
|
||||
const TOTPRegistrationAction = "RegisterTOTPDevice"
|
||||
|
||||
// U2FRegistrationAction is the string representation of the action for which the token has been produced.
|
||||
const U2FRegistrationAction = "RegisterU2FDevice"
|
||||
|
||||
// ResetPasswordAction is the string representation of the action for which the token has been produced.
|
||||
const ResetPasswordAction = "ResetPassword"
|
||||
|
||||
const authPrefix = "Basic "
|
||||
|
||||
const authorizationHeader = "Proxy-Authorization"
|
||||
const remoteUserHeader = "Remote-User"
|
||||
const remoteGroupsHeader = "Remote-Groups"
|
||||
|
||||
var protoHostSeparator = []byte("://")
|
||||
|
||||
const (
|
||||
// Forbidden means the user is forbidden the access to a resource
|
||||
Forbidden authorizationMatching = iota
|
||||
// NotAuthorized means the user can access the resource with more permissions.
|
||||
NotAuthorized authorizationMatching = iota
|
||||
// Authorized means the user is authorized given her current permissions.
|
||||
Authorized authorizationMatching = iota
|
||||
)
|
||||
|
||||
const operationFailedMessage = "Operation failed."
|
||||
const authenticationFailedMessage = "Authentication failed. Check your credentials."
|
||||
const userBannedMessage = "Please retry in a few minutes."
|
||||
const unableToRegisterOneTimePasswordMessage = "Unable to set up one-time passwords."
|
||||
const unableToRegisterSecurityKeyMessage = "Unable to register your security key."
|
||||
const unableToResetPasswordMessage = "Unable to reset your password."
|
||||
const mfaValidationFailedMessage = "Authentication failed, please retry later."
|
||||
const badBasicAuthFormatMessage = "Content of Proxy-Authorization header is wrong."
|
12
handlers/errors.go
Normal file
12
handlers/errors.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package handlers
|
||||
|
||||
import "errors"
|
||||
|
||||
// InternalError is the error message sent when there was an internal error but it should
|
||||
// be hidden to the end user. In that case the error should be in the server logs.
|
||||
const InternalError = "Internal error."
|
||||
|
||||
// UnauthorizedError is the error message sent when the user is not authorized.
|
||||
const UnauthorizedError = "You're not authorized."
|
||||
|
||||
var errMissingHeadersForTargetURL = errors.New("Missing headers for detecting target URL")
|
19
handlers/handler_2fa_available_methods.go
Normal file
19
handlers/handler_2fa_available_methods.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/clems4ever/authelia/authentication"
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
)
|
||||
|
||||
// SecondFactorAvailableMethodsGet retrieve available 2FA methods.
|
||||
// The supported methods are: "totp", "u2f", "duo"
|
||||
func SecondFactorAvailableMethodsGet(ctx *middlewares.AutheliaCtx) {
|
||||
availableMethods := MethodList{authentication.TOTP, authentication.U2F}
|
||||
|
||||
if ctx.Configuration.DuoAPI != nil {
|
||||
availableMethods = append(availableMethods, authentication.DuoPush)
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Available methods are %s", availableMethods)
|
||||
ctx.SetJSONBody(availableMethods)
|
||||
}
|
42
handlers/handler_2fa_available_methods_test.go
Normal file
42
handlers/handler_2fa_available_methods_test.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/clems4ever/authelia/mocks"
|
||||
|
||||
"github.com/clems4ever/authelia/configuration/schema"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type SecondFactorAvailableMethodsFixture struct {
|
||||
suite.Suite
|
||||
|
||||
mock *mocks.MockAutheliaCtx
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) SetupTest() {
|
||||
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TearDownTest() {
|
||||
s.mock.Close()
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethods() {
|
||||
SecondFactorAvailableMethodsGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), []string{"totp", "u2f"})
|
||||
}
|
||||
|
||||
func (s *SecondFactorAvailableMethodsFixture) TestShouldServeDefaultMethodsAndDuo() {
|
||||
s.mock.Ctx.Configuration = schema.Configuration{
|
||||
DuoAPI: &schema.DuoAPIConfiguration{},
|
||||
}
|
||||
SecondFactorAvailableMethodsGet(s.mock.Ctx)
|
||||
s.mock.Assert200OK(s.T(), []string{"totp", "u2f", "duo_push"})
|
||||
}
|
||||
|
||||
func TestRunSuite(t *testing.T) {
|
||||
s := new(SecondFactorAvailableMethodsFixture)
|
||||
suite.Run(t, s)
|
||||
}
|
68
handlers/handler_2fa_preferences.go
Normal file
68
handlers/handler_2fa_preferences.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/clems4ever/authelia/authentication"
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
)
|
||||
|
||||
// SecondFactorPreferencesGet get the user preferences regarding 2FA.
|
||||
func SecondFactorPreferencesGet(ctx *middlewares.AutheliaCtx) {
|
||||
preferences := preferences{
|
||||
Method: "totp",
|
||||
}
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
method, err := ctx.Providers.StorageProvider.LoadPrefered2FAMethod(userSession.Username)
|
||||
ctx.Logger.Debugf("Loaded prefered 2FA method of user %s is %s", userSession.Username, method)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to load prefered 2FA method: %s", err), operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if method != "" {
|
||||
// Set the retrieved method.
|
||||
preferences.Method = method
|
||||
}
|
||||
|
||||
ctx.SetJSONBody(preferences)
|
||||
}
|
||||
|
||||
func stringInSlice(a string, list []string) bool {
|
||||
for _, b := range list {
|
||||
if b == a {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SecondFactorPreferencesPost update the user preferences regarding 2FA.
|
||||
func SecondFactorPreferencesPost(ctx *middlewares.AutheliaCtx) {
|
||||
bodyJSON := preferences{}
|
||||
|
||||
err := ctx.ParseBody(&bodyJSON)
|
||||
if err != nil {
|
||||
ctx.Error(err, operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if !stringInSlice(bodyJSON.Method, authentication.PossibleMethods) {
|
||||
ctx.Error(fmt.Errorf("Unknown method %s, it should be either u2f, totp or duo_push", bodyJSON.Method), operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
ctx.Logger.Debugf("Save new prefered 2FA method of user %s to %s", userSession.Username, bodyJSON.Method)
|
||||
err = ctx.Providers.StorageProvider.SavePrefered2FAMethod(userSession.Username, bodyJSON.Method)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to save new prefered 2FA method: %s", err), operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ReplyOK()
|
||||
}
|
129
handlers/handler_2fa_preferences_test.go
Normal file
129
handlers/handler_2fa_preferences_test.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/clems4ever/authelia/mocks"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type SecondFactorPreferencesSuite struct {
|
||||
suite.Suite
|
||||
|
||||
mock *mocks.MockAutheliaCtx
|
||||
}
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) SetupTest() {
|
||||
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||
// Set the intial user session.
|
||||
userSession := s.mock.Ctx.GetSession()
|
||||
userSession.Username = "john"
|
||||
userSession.AuthenticationLevel = 1
|
||||
s.mock.Ctx.SaveSession(userSession)
|
||||
}
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TearDownTest() {
|
||||
s.mock.Close()
|
||||
}
|
||||
|
||||
// GET
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TestShouldGetPreferenceRetrievedFromStorage() {
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
LoadPrefered2FAMethod(gomock.Eq("john")).
|
||||
Return("u2f", nil)
|
||||
SecondFactorPreferencesGet(s.mock.Ctx)
|
||||
|
||||
s.mock.Assert200OK(s.T(), preferences{Method: "u2f"})
|
||||
}
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TestShouldGetDefaultPreferenceIfNotInDB() {
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
LoadPrefered2FAMethod(gomock.Eq("john")).
|
||||
Return("", nil)
|
||||
SecondFactorPreferencesGet(s.mock.Ctx)
|
||||
|
||||
s.mock.Assert200OK(s.T(), preferences{Method: "totp"})
|
||||
}
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenStorageFailsToLoad() {
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
LoadPrefered2FAMethod(gomock.Eq("john")).
|
||||
Return("", fmt.Errorf("Failure"))
|
||||
SecondFactorPreferencesGet(s.mock.Ctx)
|
||||
|
||||
s.mock.Assert200KO(s.T(), "Operation failed.")
|
||||
assert.Equal(s.T(), "Unable to load prefered 2FA method: Failure", s.mock.Hook.LastEntry().Message)
|
||||
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
||||
}
|
||||
|
||||
// POST
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenNoBodyProvided() {
|
||||
SecondFactorPreferencesPost(s.mock.Ctx)
|
||||
|
||||
s.mock.Assert200KO(s.T(), "Operation failed.")
|
||||
assert.Equal(s.T(), "Unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message)
|
||||
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
||||
}
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenMalformedBodyProvided() {
|
||||
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"abc\""))
|
||||
SecondFactorPreferencesPost(s.mock.Ctx)
|
||||
|
||||
s.mock.Assert200KO(s.T(), "Operation failed.")
|
||||
assert.Equal(s.T(), "Unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message)
|
||||
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
||||
}
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenBadBodyProvided() {
|
||||
s.mock.Ctx.Request.SetBody([]byte("{\"weird_key\":\"abc\"}"))
|
||||
SecondFactorPreferencesPost(s.mock.Ctx)
|
||||
|
||||
s.mock.Assert200KO(s.T(), "Operation failed.")
|
||||
assert.Equal(s.T(), "Unable to validate body: method: non zero value required", s.mock.Hook.LastEntry().Message)
|
||||
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
||||
}
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenBadMethodProvided() {
|
||||
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"abc\"}"))
|
||||
SecondFactorPreferencesPost(s.mock.Ctx)
|
||||
|
||||
s.mock.Assert200KO(s.T(), "Operation failed.")
|
||||
assert.Equal(s.T(), "Unknown method abc, it should be either u2f, totp or duo_push", s.mock.Hook.LastEntry().Message)
|
||||
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
||||
}
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TestShouldReturnError500WhenDatabaseFailsToSave() {
|
||||
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}"))
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
SavePrefered2FAMethod(gomock.Eq("john"), gomock.Eq("u2f")).
|
||||
Return(fmt.Errorf("Failure"))
|
||||
|
||||
SecondFactorPreferencesPost(s.mock.Ctx)
|
||||
|
||||
s.mock.Assert200KO(s.T(), "Operation failed.")
|
||||
assert.Equal(s.T(), "Unable to save new prefered 2FA method: Failure", s.mock.Hook.LastEntry().Message)
|
||||
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
||||
}
|
||||
|
||||
func (s *SecondFactorPreferencesSuite) TestShouldReturn200WhenMethodIsSuccessfullySaved() {
|
||||
s.mock.Ctx.Request.SetBody([]byte("{\"method\":\"u2f\"}"))
|
||||
s.mock.StorageProviderMock.EXPECT().
|
||||
SavePrefered2FAMethod(gomock.Eq("john"), gomock.Eq("u2f")).
|
||||
Return(nil)
|
||||
|
||||
SecondFactorPreferencesPost(s.mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
func TestRunPreferencesSuite(t *testing.T) {
|
||||
s := new(SecondFactorPreferencesSuite)
|
||||
suite.Run(t, s)
|
||||
}
|
133
handlers/handler_firstfactor.go
Normal file
133
handlers/handler_firstfactor.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/clems4ever/authelia/regulation"
|
||||
|
||||
"github.com/clems4ever/authelia/session"
|
||||
|
||||
"github.com/clems4ever/authelia/authentication"
|
||||
"github.com/clems4ever/authelia/authorization"
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
)
|
||||
|
||||
// FirstFactorPost is the handler performing the first factory.
|
||||
func FirstFactorPost(ctx *middlewares.AutheliaCtx) {
|
||||
bodyJSON := firstFactorRequestBody{}
|
||||
err := ctx.ParseBody(&bodyJSON)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(err, authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
bannedUntil, err := ctx.Providers.Regulator.Regulate(bodyJSON.Username)
|
||||
|
||||
if err == regulation.ErrUserIsBanned {
|
||||
ctx.Error(fmt.Errorf("User %s is banned until %s", bodyJSON.Username, bannedUntil), userBannedMessage)
|
||||
return
|
||||
} else if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to regulate authentication: %s", err), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
userPasswordOk, err := ctx.Providers.UserProvider.CheckUserPassword(bodyJSON.Username, bodyJSON.Password)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Error while checking password for user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Mark authentication attempt made by user %s", bodyJSON.Username)
|
||||
// Mark the authentication attempt and whether it was successful.
|
||||
err = ctx.Providers.Regulator.Mark(bodyJSON.Username, userPasswordOk)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to mark authentication: %s", err), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if !userPasswordOk {
|
||||
ctx.Error(fmt.Errorf("Credentials are wrong for user %s", bodyJSON.Username), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Credentials validation of user %s is ok", bodyJSON.Username)
|
||||
|
||||
// Reset all values from previous session before regenerating the cookie.
|
||||
err = ctx.SaveSession(session.NewDefaultUserSession())
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to reset the session for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.Providers.SessionProvider.RegenerateSession(ctx.RequestCtx)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to regenerate session for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
// and avoid the cookie to expire if "Remember me" was ticked.
|
||||
if *bodyJSON.KeepMeLoggedIn {
|
||||
err = ctx.Providers.SessionProvider.UpdateExpiration(ctx.RequestCtx, time.Duration(0))
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to update expiration timer for user %s: %s", bodyJSON.Username, err), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get the details of the given user from the user provider.
|
||||
userDetails, err := ctx.Providers.UserProvider.GetDetails(bodyJSON.Username)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Error while retrieving details from user %s: %s", bodyJSON.Username, err.Error()), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Details for user %s => groups: %s, emails %s", bodyJSON.Username, userDetails.Groups, userDetails.Emails)
|
||||
|
||||
// And set those information in the new session.
|
||||
userSession := ctx.GetSession()
|
||||
userSession.Username = bodyJSON.Username
|
||||
userSession.Groups = userDetails.Groups
|
||||
userSession.Emails = userDetails.Emails
|
||||
userSession.AuthenticationLevel = authentication.OneFactor
|
||||
userSession.LastActivity = time.Now().Unix()
|
||||
err = ctx.SaveSession(userSession)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to save session of user %s", bodyJSON.Username), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if bodyJSON.TargetURL != "" {
|
||||
targetURL, err := url.ParseRequestURI(bodyJSON.TargetURL)
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to parse target URL %s: %s", bodyJSON.TargetURL, err), authenticationFailedMessage)
|
||||
return
|
||||
}
|
||||
requiredLevel := ctx.Providers.Authorizer.GetRequiredLevel(authorization.Subject{
|
||||
Username: userSession.Username,
|
||||
Groups: userSession.Groups,
|
||||
IP: ctx.RemoteIP(),
|
||||
}, *targetURL)
|
||||
|
||||
ctx.Logger.Debugf("Required level for the URL %s is %d", targetURL.String(), requiredLevel)
|
||||
|
||||
safeRedirection := isRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain)
|
||||
|
||||
if safeRedirection && requiredLevel <= authorization.OneFactor {
|
||||
response := redirectResponse{bodyJSON.TargetURL}
|
||||
ctx.SetJSONBody(response)
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
}
|
169
handlers/handler_firstfactor_test.go
Normal file
169
handlers/handler_firstfactor_test.go
Normal file
|
@ -0,0 +1,169 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/clems4ever/authelia/mocks"
|
||||
|
||||
"github.com/clems4ever/authelia/authentication"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type FirstFactorSuite struct {
|
||||
suite.Suite
|
||||
|
||||
mock *mocks.MockAutheliaCtx
|
||||
}
|
||||
|
||||
func (s *FirstFactorSuite) SetupTest() {
|
||||
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||
}
|
||||
|
||||
func (s *FirstFactorSuite) TearDownTest() {
|
||||
s.mock.Close()
|
||||
}
|
||||
|
||||
func (s *FirstFactorSuite) assertError500(err string) {
|
||||
assert.Equal(s.T(), 500, s.mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(s.T(), []byte(InternalError), s.mock.Ctx.Response.Body())
|
||||
assert.Equal(s.T(), err, s.mock.Hook.LastEntry().Message)
|
||||
assert.Equal(s.T(), logrus.ErrorLevel, s.mock.Hook.LastEntry().Level)
|
||||
}
|
||||
|
||||
func (s *FirstFactorSuite) TestShouldFailIfBodyIsNil() {
|
||||
FirstFactorPost(s.mock.Ctx)
|
||||
|
||||
// No body
|
||||
assert.Equal(s.T(), "Unable to parse body: unexpected end of JSON input", s.mock.Hook.LastEntry().Message)
|
||||
s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
|
||||
}
|
||||
|
||||
func (s *FirstFactorSuite) TestShouldFailIfBodyIsInBadFormat() {
|
||||
// Missing password
|
||||
s.mock.Ctx.Request.SetBodyString(`{
|
||||
"username": "test"
|
||||
}`)
|
||||
FirstFactorPost(s.mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), "Unable to validate body: password: non zero value required", s.mock.Hook.LastEntry().Message)
|
||||
s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
|
||||
}
|
||||
|
||||
func (s *FirstFactorSuite) TestShouldFailIfUserProviderCheckPasswordFail() {
|
||||
s.mock.UserProviderMock.
|
||||
EXPECT().
|
||||
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
||||
Return(false, fmt.Errorf("Failed"))
|
||||
|
||||
s.mock.Ctx.Request.SetBodyString(`{
|
||||
"username": "test",
|
||||
"password": "hello",
|
||||
"keepMeLoggedIn": true
|
||||
}`)
|
||||
FirstFactorPost(s.mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), "Error while checking password for user test: Failed", s.mock.Hook.LastEntry().Message)
|
||||
s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
|
||||
}
|
||||
|
||||
func (s *FirstFactorSuite) TestShouldFailIfUserProviderGetDetailsFail() {
|
||||
s.mock.UserProviderMock.
|
||||
EXPECT().
|
||||
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
||||
Return(true, nil)
|
||||
|
||||
s.mock.UserProviderMock.
|
||||
EXPECT().
|
||||
GetDetails(gomock.Eq("test")).
|
||||
Return(nil, fmt.Errorf("Failed"))
|
||||
|
||||
s.mock.StorageProviderMock.
|
||||
EXPECT().
|
||||
AppendAuthenticationLog(gomock.Any()).
|
||||
Return(nil)
|
||||
|
||||
s.mock.Ctx.Request.SetBodyString(`{
|
||||
"username": "test",
|
||||
"password": "hello",
|
||||
"keepMeLoggedIn": true
|
||||
}`)
|
||||
FirstFactorPost(s.mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), "Error while retrieving details from user test: Failed", s.mock.Hook.LastEntry().Message)
|
||||
s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
|
||||
}
|
||||
|
||||
func (s *FirstFactorSuite) TestShouldFailIfAuthenticationLoggingFail() {
|
||||
s.mock.UserProviderMock.
|
||||
EXPECT().
|
||||
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
||||
Return(true, nil)
|
||||
|
||||
s.mock.UserProviderMock.
|
||||
EXPECT().
|
||||
GetDetails(gomock.Eq("test")).
|
||||
Return(nil, nil)
|
||||
|
||||
s.mock.StorageProviderMock.
|
||||
EXPECT().
|
||||
AppendAuthenticationLog(gomock.Any()).
|
||||
Return(fmt.Errorf("failed"))
|
||||
|
||||
s.mock.Ctx.Request.SetBodyString(`{
|
||||
"username": "test",
|
||||
"password": "hello",
|
||||
"keepMeLoggedIn": true
|
||||
}`)
|
||||
FirstFactorPost(s.mock.Ctx)
|
||||
|
||||
assert.Equal(s.T(), "Unable to mark authentication: failed", s.mock.Hook.LastEntry().Message)
|
||||
s.mock.Assert200KO(s.T(), "Authentication failed. Check your credentials.")
|
||||
}
|
||||
|
||||
func (s *FirstFactorSuite) TestShouldAuthenticateUser() {
|
||||
s.mock.UserProviderMock.
|
||||
EXPECT().
|
||||
CheckUserPassword(gomock.Eq("test"), gomock.Eq("hello")).
|
||||
Return(true, nil)
|
||||
|
||||
s.mock.UserProviderMock.
|
||||
EXPECT().
|
||||
GetDetails(gomock.Eq("test")).
|
||||
Return(&authentication.UserDetails{
|
||||
Emails: []string{"test@example.com"},
|
||||
Groups: []string{"dev", "admin"},
|
||||
}, nil)
|
||||
|
||||
s.mock.StorageProviderMock.
|
||||
EXPECT().
|
||||
AppendAuthenticationLog(gomock.Any()).
|
||||
Return(nil)
|
||||
|
||||
s.mock.Ctx.Request.SetBodyString(`{
|
||||
"username": "test",
|
||||
"password": "hello",
|
||||
"keepMeLoggedIn": true
|
||||
}`)
|
||||
FirstFactorPost(s.mock.Ctx)
|
||||
|
||||
// Respond with 200.
|
||||
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
|
||||
assert.Equal(s.T(), []byte("{\"status\":\"OK\"}"), s.mock.Ctx.Response.Body())
|
||||
|
||||
// And store authentication in session.
|
||||
session := s.mock.Ctx.GetSession()
|
||||
assert.Equal(s.T(), "test", session.Username)
|
||||
assert.Equal(s.T(), authentication.OneFactor, session.AuthenticationLevel)
|
||||
assert.Equal(s.T(), []string{"test@example.com"}, session.Emails)
|
||||
assert.Equal(s.T(), []string{"dev", "admin"}, session.Groups)
|
||||
|
||||
}
|
||||
|
||||
func TestFirstFactorSuite(t *testing.T) {
|
||||
firstFactorSuite := new(FirstFactorSuite)
|
||||
suite.Run(t, firstFactorSuite)
|
||||
}
|
19
handlers/handler_logout.go
Normal file
19
handlers/handler_logout.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
)
|
||||
|
||||
// LogoutPost is the handler logging out the user attached to the given cookie.
|
||||
func LogoutPost(ctx *middlewares.AutheliaCtx) {
|
||||
ctx.Logger.Debug("Destroy session")
|
||||
err := ctx.Providers.SessionProvider.DestroySession(ctx.RequestCtx)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to destroy session during logout: %s", err), operationFailedMessage)
|
||||
}
|
||||
|
||||
ctx.ReplyOK()
|
||||
}
|
43
handlers/handler_logout_test.go
Normal file
43
handlers/handler_logout_test.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/clems4ever/authelia/mocks"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type LogoutSuite struct {
|
||||
suite.Suite
|
||||
|
||||
mock *mocks.MockAutheliaCtx
|
||||
}
|
||||
|
||||
func (s *LogoutSuite) SetupTest() {
|
||||
s.mock = mocks.NewMockAutheliaCtx(s.T())
|
||||
userSession := s.mock.Ctx.GetSession()
|
||||
userSession.Username = "john"
|
||||
s.mock.Ctx.SaveSession(userSession)
|
||||
}
|
||||
|
||||
func (s *LogoutSuite) TearDownTest() {
|
||||
s.mock.Close()
|
||||
}
|
||||
|
||||
func (s *LogoutSuite) TestShouldDestroySession() {
|
||||
LogoutPost(s.mock.Ctx)
|
||||
b := s.mock.Ctx.Response.Header.PeekCookie("authelia_session")
|
||||
|
||||
// Reset the cookie, meaning it resets the value and expires the cookie by setting
|
||||
// date to one minute in the past.
|
||||
assert.True(s.T(), strings.HasPrefix(string(b), "authelia_session=;"))
|
||||
}
|
||||
|
||||
func TestRunLogoutSuite(t *testing.T) {
|
||||
s := new(LogoutSuite)
|
||||
suite.Run(t, s)
|
||||
}
|
70
handlers/handler_register_totp.go
Normal file
70
handlers/handler_register_totp.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
"github.com/clems4ever/authelia/session"
|
||||
"github.com/pquerna/otp/totp"
|
||||
)
|
||||
|
||||
// identityRetrieverFromSession retriever computing the identity from the cookie session.
|
||||
func identityRetrieverFromSession(ctx *middlewares.AutheliaCtx) (*session.Identity, error) {
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if len(userSession.Emails) == 0 {
|
||||
return nil, fmt.Errorf("User %s does not have any email address", userSession.Username)
|
||||
}
|
||||
|
||||
return &session.Identity{
|
||||
Username: userSession.Username,
|
||||
Email: userSession.Emails[0],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isTokenUserValidFor2FARegistration(ctx *middlewares.AutheliaCtx, username string) bool {
|
||||
return ctx.GetSession().Username == username
|
||||
}
|
||||
|
||||
// SecondFactorTOTPIdentityStart the handler for initiating the identity validation.
|
||||
var SecondFactorTOTPIdentityStart = middlewares.IdentityVerificationStart(middlewares.IdentityVerificationStartArgs{
|
||||
MailSubject: "[Authelia] Register your mobile",
|
||||
MailTitle: "Register your mobile",
|
||||
MailButtonContent: "Register",
|
||||
TargetEndpoint: "/one-time-password-registration",
|
||||
ActionClaim: TOTPRegistrationAction,
|
||||
IdentityRetrieverFunc: identityRetrieverFromSession,
|
||||
})
|
||||
|
||||
func secondFactorTOTPIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: ctx.Configuration.TOTP.Issuer,
|
||||
AccountName: username,
|
||||
SecretSize: 32,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to generate TOTP key: %s", err), unableToRegisterOneTimePasswordMessage)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.Providers.StorageProvider.SaveTOTPSecret(username, key.Secret())
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to save TOTP secret in DB: %s", err), unableToRegisterOneTimePasswordMessage)
|
||||
return
|
||||
}
|
||||
|
||||
response := TOTPKeyResponse{
|
||||
OTPAuthURL: key.URL(),
|
||||
Base32Secret: key.Secret(),
|
||||
}
|
||||
|
||||
ctx.SetJSONBody(response)
|
||||
}
|
||||
|
||||
// SecondFactorTOTPIdentityFinish the handler for finishing the identity validation
|
||||
var SecondFactorTOTPIdentityFinish = middlewares.IdentityVerificationFinish(
|
||||
middlewares.IdentityVerificationFinishArgs{
|
||||
ActionClaim: TOTPRegistrationAction,
|
||||
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
|
||||
}, secondFactorTOTPIdentityFinish)
|
63
handlers/handler_register_u2f_step1.go
Normal file
63
handlers/handler_register_u2f_step1.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
"github.com/tstranex/u2f"
|
||||
)
|
||||
|
||||
var u2fConfig = &u2f.Config{
|
||||
// Chrome 66+ doesn't return the device's attestation
|
||||
// certificate by default.
|
||||
SkipAttestationVerify: true,
|
||||
}
|
||||
|
||||
// SecondFactorU2FIdentityStart the handler for initiating the identity validation.
|
||||
var SecondFactorU2FIdentityStart = middlewares.IdentityVerificationStart(middlewares.IdentityVerificationStartArgs{
|
||||
MailSubject: "[Authelia] Register your key",
|
||||
MailTitle: "Register your key",
|
||||
MailButtonContent: "Register",
|
||||
TargetEndpoint: "/security-key-registration",
|
||||
ActionClaim: U2FRegistrationAction,
|
||||
IdentityRetrieverFunc: identityRetrieverFromSession,
|
||||
})
|
||||
|
||||
func secondFactorU2FIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
|
||||
appID := fmt.Sprintf("%s://%s", ctx.XForwardedProto(), ctx.XForwardedHost())
|
||||
ctx.Logger.Debugf("U2F appID is %s", appID)
|
||||
var trustedFacets = []string{appID}
|
||||
|
||||
challenge, err := u2f.NewChallenge(appID, trustedFacets)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to generate new U2F challenge for registration: %s", err), operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
// Save the challenge in the user session.
|
||||
userSession := ctx.GetSession()
|
||||
userSession.U2FChallenge = challenge
|
||||
err = ctx.SaveSession(userSession)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to save U2F challenge in session: %s", err), operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
request := u2f.NewWebRegisterRequest(challenge, []u2f.Registration{})
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to generate new U2F request for registration: %s", err), operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetJSONBody(request)
|
||||
}
|
||||
|
||||
// SecondFactorU2FIdentityFinish the handler for finishing the identity validation
|
||||
var SecondFactorU2FIdentityFinish = middlewares.IdentityVerificationFinish(
|
||||
middlewares.IdentityVerificationFinishArgs{
|
||||
ActionClaim: U2FRegistrationAction,
|
||||
IsTokenUserValidFunc: isTokenUserValidFor2FARegistration,
|
||||
}, secondFactorU2FIdentityFinish)
|
50
handlers/handler_register_u2f_step2.go
Normal file
50
handlers/handler_register_u2f_step2.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
"github.com/tstranex/u2f"
|
||||
)
|
||||
|
||||
// SecondFactorU2FRegister handler validating the client has successfully validated the challenge
|
||||
// to complete the U2F registration.
|
||||
func SecondFactorU2FRegister(ctx *middlewares.AutheliaCtx) {
|
||||
responseBody := u2f.RegisterResponse{}
|
||||
err := ctx.ParseBody(&responseBody)
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
if userSession.U2FChallenge == nil {
|
||||
ctx.Error(fmt.Errorf("U2F registration has not been initiated yet"), unableToRegisterSecurityKeyMessage)
|
||||
return
|
||||
}
|
||||
// Ensure the challenge is cleared if anything goes wrong.
|
||||
defer func() {
|
||||
userSession.U2FChallenge = nil
|
||||
ctx.SaveSession(userSession)
|
||||
}()
|
||||
|
||||
registration, err := u2f.Register(responseBody, *userSession.U2FChallenge, u2fConfig)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to verify U2F registration: %v", err), unableToRegisterSecurityKeyMessage)
|
||||
return
|
||||
}
|
||||
|
||||
deviceHandle, err := registration.MarshalBinary()
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to marshal U2F registration data: %v", err), unableToRegisterSecurityKeyMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Register U2F device for user %s", userSession.Username)
|
||||
err = ctx.Providers.StorageProvider.SaveU2FDeviceHandle(userSession.Username, deviceHandle)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to register U2F device for user %s: %v", userSession.Username, err), unableToRegisterSecurityKeyMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ReplyOK()
|
||||
}
|
57
handlers/handler_reset_password_step1.go
Normal file
57
handlers/handler_reset_password_step1.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
"github.com/clems4ever/authelia/session"
|
||||
)
|
||||
|
||||
func identityRetrieverFromStorage(ctx *middlewares.AutheliaCtx) (*session.Identity, error) {
|
||||
var requestBody resetPasswordStep1RequestBody
|
||||
err := json.Unmarshal(ctx.PostBody(), &requestBody)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
details, err := ctx.Providers.UserProvider.GetDetails(requestBody.Username)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(details.Emails) == 0 {
|
||||
return nil, fmt.Errorf("User %s has no email address configured", requestBody.Username)
|
||||
}
|
||||
|
||||
return &session.Identity{
|
||||
Username: requestBody.Username,
|
||||
Email: details.Emails[0],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ResetPasswordIdentityStart the handler for initiating the identity validation for resetting a password.
|
||||
// We need to ensure the attacker cannot perform user enumeration by alway replying with 200 whatever what happens in backend.
|
||||
var ResetPasswordIdentityStart = middlewares.IdentityVerificationStart(middlewares.IdentityVerificationStartArgs{
|
||||
MailSubject: "[Authelia] Reset your password",
|
||||
MailTitle: "Reset your password",
|
||||
MailButtonContent: "Reset",
|
||||
TargetEndpoint: "/reset-password",
|
||||
ActionClaim: ResetPasswordAction,
|
||||
IdentityRetrieverFunc: identityRetrieverFromStorage,
|
||||
})
|
||||
|
||||
func resetPasswordIdentityFinish(ctx *middlewares.AutheliaCtx, username string) {
|
||||
userSession := ctx.GetSession()
|
||||
// TODO(c.michaud): use JWT tokens to expire the request in only few seconds for better security.
|
||||
userSession.PasswordResetUsername = &username
|
||||
ctx.SaveSession(userSession)
|
||||
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
|
||||
// ResetPasswordIdentityFinish the handler for finishing the identity validation
|
||||
var ResetPasswordIdentityFinish = middlewares.IdentityVerificationFinish(
|
||||
middlewares.IdentityVerificationFinishArgs{ActionClaim: ResetPasswordAction}, resetPasswordIdentityFinish)
|
48
handlers/handler_reset_password_step2.go
Normal file
48
handlers/handler_reset_password_step2.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
)
|
||||
|
||||
// ResetPasswordPost handler for resetting passwords
|
||||
func ResetPasswordPost(ctx *middlewares.AutheliaCtx) {
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
// Those checks unsure that the identity verification process has been initiated and completed successfully
|
||||
// otherwise PasswordReset would not be set to true. We can improve the security of this check by making the
|
||||
// request expire at some point because here it only expires when the cookie expires...
|
||||
if userSession.PasswordResetUsername == nil {
|
||||
ctx.Error(fmt.Errorf("No identity verification process has been initiated"), unableToResetPasswordMessage)
|
||||
return
|
||||
}
|
||||
|
||||
var requestBody resetPasswordStep2RequestBody
|
||||
err := ctx.ParseBody(&requestBody)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(err, unableToResetPasswordMessage)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctx.Providers.UserProvider.UpdatePassword(*userSession.PasswordResetUsername, requestBody.Password)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to update password: %s", err), unableToResetPasswordMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Logger.Debugf("Password of user %s has been reset", *userSession.PasswordResetUsername)
|
||||
|
||||
// Reset the request.
|
||||
userSession.PasswordResetUsername = nil
|
||||
err = ctx.SaveSession(userSession)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to update password reset state: %s", err), operationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.ReplyOK()
|
||||
}
|
71
handlers/handler_sign_duo.go
Normal file
71
handlers/handler_sign_duo.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/clems4ever/authelia/authentication"
|
||||
"github.com/clems4ever/authelia/duo"
|
||||
"github.com/clems4ever/authelia/middlewares"
|
||||
)
|
||||
|
||||
// SecondFactorDuoPost handler for sending a push notification via duo api.
|
||||
func SecondFactorDuoPost(duoAPI duo.API) middlewares.RequestHandler {
|
||||
return func(ctx *middlewares.AutheliaCtx) {
|
||||
var requestBody signDuoRequestBody
|
||||
err := ctx.ParseBody(&requestBody)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(err, mfaValidationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
userSession := ctx.GetSession()
|
||||
|
||||
values := url.Values{}
|
||||
// { username, ipaddr: clientIP, factor: "push", device: "auto", pushinfo: `target%20url=${targetURL}`}
|
||||
values.Set("username", userSession.Username)
|
||||
values.Set("ipaddr", ctx.RemoteIP().String())
|
||||
values.Set("factor", "push")
|
||||
values.Set("device", "auto")
|
||||
if requestBody.TargetURL != "" {
|
||||
values.Set("pushinfo", fmt.Sprintf("target%%20url=%s", requestBody.TargetURL))
|
||||
}
|
||||
|
||||
duoResponse, err := duoAPI.Call(values)
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Duo API errored: %s", err), mfaValidationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if duoResponse.Response.Result != "allow" {
|
||||
ctx.ReplyUnauthorized()
|
||||
return
|
||||
}
|
||||
|
||||
userSession.AuthenticationLevel = authentication.TwoFactor
|
||||
err = ctx.SaveSession(userSession)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to update authentication level with Duo: %s", err), mfaValidationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if requestBody.TargetURL != "" {
|
||||
targetURL, err := url.ParseRequestURI(requestBody.TargetURL)
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(fmt.Errorf("Unable to parse target URL: %s", err), mfaValidationFailedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if targetURL != nil && isRedirectionSafe(*targetURL, ctx.Configuration.Session.Domain) {
|
||||
ctx.SetJSONBody(redirectResponse{Redirect: requestBody.TargetURL})
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
} else {
|
||||
ctx.ReplyOK()
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user