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:
Clement Michaud 2019-04-24 23:52:08 +02:00 committed by Clément Michaud
parent 325076a827
commit 828f565290
435 changed files with 14191 additions and 20783 deletions

5
.gitignore vendored
View File

@ -40,3 +40,8 @@ users_database.test.yml
.suite .suite
.kube .kube
.idea .idea
# Go binary
authelia
.authelia-interrupt

View File

@ -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

View File

@ -1,7 +1,7 @@
language: node_js language: go
required: sudo required: sudo
node_js: go:
- '9' - '1.13'
services: services:
- docker - docker
- ntp - ntp
@ -12,9 +12,11 @@ addons:
sources: sources:
- google-chrome - google-chrome
packages: packages:
- xvfb
- libgif-dev - libgif-dev
- google-chrome-stable - 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: script:
- "./scripts/authelia-scripts travis" - "./scripts/authelia-scripts travis"
after_success: after_success:

View File

@ -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 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. 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 ## Breaking in v3.14.0
### Headers in nginx configuration ### Headers in nginx configuration

View File

@ -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 \ # Install the libc required by the password hashing compiled with CGO.
.build-deps make g++ python && \ RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
npm install --production && \ RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk
apk del .build-deps 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 EXPOSE 9091
VOLUME /etc/authelia VOLUME /etc/authelia
VOLUME /var/lib/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
View 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}

View 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
}

View 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
`)

View 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
}

View 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
}

View 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
View File

@ -0,0 +1,7 @@
package authentication
// UserDetails represent the details retrieved for a given user.
type UserDetails struct {
Emails []string
Groups []string
}

View 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
View 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)
}

View 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
View 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
)

View File

@ -3,7 +3,6 @@ export PATH=$(pwd)/scripts:/tmp:$PATH
export PS1="(authelia) $PS1" export PS1="(authelia) $PS1"
echo "[BOOTSTRAP] Installing npm packages..."
npm i npm i
pushd client pushd client
@ -27,5 +26,8 @@ fi
echo "[BOOTSTRAP] Running additional bootstrap steps..." echo "[BOOTSTRAP] Running additional bootstrap steps..."
authelia-scripts bootstrap 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] 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" echo "[BOOTSTRAP] More details at https://github.com/clems4ever/authelia/blob/master/docs/getting-started.md"

7538
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,13 +17,12 @@
"@types/react-redux": "^6.0.12", "@types/react-redux": "^6.0.12",
"@types/react-router-dom": "^4.3.1", "@types/react-router-dom": "^4.3.1",
"@types/redux-thunk": "^2.1.0", "@types/redux-thunk": "^2.1.0",
"await-to-js": "^2.1.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"connected-react-router": "^6.2.1", "connected-react-router": "^6.2.1",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"qrcode.react": "^0.9.2", "qrcode.react": "^0.9.2",
"query-string": "^6.2.0", "query-string": "^6.2.0",
"react": "^16.6.0", "react": "^16.10.2",
"react-dom": "^16.6.0", "react-dom": "^16.6.0",
"react-redux": "^6.0.0", "react-redux": "^6.0.0",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",

View File

@ -6,6 +6,7 @@ export default async function(dispatch: Dispatch) {
dispatch(getPreferedMethod()); dispatch(getPreferedMethod());
try { try {
const method = await AutheliaService.fetchPrefered2faMethod(); const method = await AutheliaService.fetchPrefered2faMethod();
console.log(method);
dispatch(getPreferedMethodSuccess(method)); dispatch(getPreferedMethodSuccess(method));
} catch (err) { } catch (err) {
dispatch(getPreferedMethodFailure(err.message)) dispatch(getPreferedMethodFailure(err.message))

View File

@ -1,19 +1,12 @@
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { fetchStateFailure, fetchStateSuccess } from "../reducers/Portal/Authentication/actions"; import { fetchStateFailure, fetchStateSuccess } from "../reducers/Portal/Authentication/actions";
import to from "await-to-js";
import AutheliaService from "../services/AutheliaService"; import AutheliaService from "../services/AutheliaService";
export default async function(dispatch: Dispatch) { export default async function(dispatch: Dispatch) {
let err, res; try {
[err, res] = await to(AutheliaService.fetchState()); const state = await AutheliaService.fetchState();
if (err) { dispatch(fetchStateSuccess(state));
await dispatch(fetchStateFailure(err.message)); } catch (err) {
return; dispatch(fetchStateFailure(err.message));
} }
if (!res) {
await dispatch(fetchStateFailure('No response'));
return
}
await dispatch(fetchStateSuccess(res));
return res;
} }

View File

@ -1,18 +1,15 @@
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { logout, logoutFailure, logoutSuccess } from "../reducers/Portal/SecondFactor/actions"; import { logout, logoutFailure, logoutSuccess } from "../reducers/Portal/SecondFactor/actions";
import to from "await-to-js";
import fetchState from "./FetchStateBehavior"; import fetchState from "./FetchStateBehavior";
import AutheliaService from "../services/AutheliaService"; import AutheliaService from "../services/AutheliaService";
export default async function(dispatch: Dispatch) { export default async function(dispatch: Dispatch) {
await dispatch(logout()); try {
let err, res; dispatch(logout());
[err, res] = await to(AutheliaService.postLogout()); await AutheliaService.postLogout();
dispatch(logoutSuccess());
if (err) { await fetchState(dispatch);
await dispatch(logoutFailure(err.message)); } catch (err) {
return; dispatch(logoutFailure(err.message));
} }
await dispatch(logoutSuccess());
await fetchState(dispatch);
} }

View File

@ -17,17 +17,19 @@ export interface OwnProps {
export interface StateProps { export interface StateProps {
formDisabled: boolean; formDisabled: boolean;
error: string | null; error: string | null;
username: string;
password: string;
} }
export interface DispatchProps { 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; export type Props = OwnProps & StateProps & DispatchProps;
interface State { interface State {
username: string;
password: string;
rememberMe: boolean; rememberMe: boolean;
} }
@ -35,8 +37,6 @@ class FirstFactorForm extends Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props) super(props)
this.state = { this.state = {
username: '',
password: '',
rememberMe: false, rememberMe: false,
} }
} }
@ -49,12 +49,12 @@ class FirstFactorForm extends Component<Props, State> {
onUsernameChanged = (e: FormEvent<HTMLElement>) => { onUsernameChanged = (e: FormEvent<HTMLElement>) => {
const val = (e.target as HTMLInputElement).value; const val = (e.target as HTMLInputElement).value;
this.setState({username: val}); this.props.onUsernameChanged(val);
} }
onPasswordChanged = (e: FormEvent<HTMLElement>) => { onPasswordChanged = (e: FormEvent<HTMLElement>) => {
const val = (e.target as HTMLInputElement).value; const val = (e.target as HTMLInputElement).value;
this.setState({password: val}); this.props.onPasswordChanged(val);
} }
onLoginClicked = () => { onLoginClicked = () => {
@ -83,9 +83,10 @@ class FirstFactorForm extends Component<Props, State> {
outlined={true}> outlined={true}>
<Input <Input
id="username" id="username"
name="username"
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
disabled={this.props.formDisabled} disabled={this.props.formDisabled}
value={this.state.username}/> value={this.props.username}/>
</TextField> </TextField>
</div> </div>
<div className={styles.field}> <div className={styles.field}>
@ -95,11 +96,12 @@ class FirstFactorForm extends Component<Props, State> {
outlined={true}> outlined={true}>
<Input <Input
id="password" id="password"
name="password"
type="password" type="password"
disabled={this.props.formDisabled} disabled={this.props.formDisabled}
onChange={this.onPasswordChanged} onChange={this.onPasswordChanged}
onKeyPress={this.onPasswordKeyPressed} onKeyPress={this.onPasswordKeyPressed}
value={this.state.password} /> value={this.props.password} />
</TextField> </TextField>
</div> </div>
</div> </div>
@ -134,13 +136,9 @@ class FirstFactorForm extends Component<Props, State> {
private authenticate() { private authenticate() {
this.props.onAuthenticationRequested( this.props.onAuthenticationRequested(
this.state.username, this.props.username,
this.state.password, this.props.password,
this.state.rememberMe) this.state.rememberMe)
.catch((err: Error) => console.error(err))
.finally(() => {
this.setState({username: '', password: ''});
})
} }
} }

View File

@ -1,9 +1,14 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Dispatch } from '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 FirstFactorForm, { StateProps, OwnProps } from '../../../components/FirstFactorForm/FirstFactorForm';
import { RootState } from '../../../reducers'; import { RootState } from '../../../reducers';
import to from 'await-to-js';
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior'; import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
import AutheliaService from '../../../services/AutheliaService'; import AutheliaService from '../../../services/AutheliaService';
@ -11,57 +16,42 @@ const mapStateToProps = (state: RootState): StateProps => {
return { return {
error: state.firstFactor.error, error: state.firstFactor.error,
formDisabled: state.firstFactor.loading, formDisabled: state.firstFactor.loading,
username: state.firstFactor.username,
password: state.firstFactor.password,
}; };
} }
function onAuthenticationRequested(dispatch: Dispatch, redirectionUrl: string | null) { function onAuthenticationRequested(dispatch: Dispatch, redirectionUrl: string | null) {
return async (username: string, password: string, rememberMe: boolean): Promise<void> => { return async (username: string, password: string, rememberMe: boolean): Promise<void> => {
let err, res;
// Validate first factor // Validate first factor
dispatch(authenticate()); dispatch(authenticate());
[err, res] = await to(AutheliaService.postFirstFactorAuth( try {
username, password, rememberMe, redirectionUrl)); const redirectOrUndefined = await AutheliaService.postFirstFactorAuth(
username, password, rememberMe, redirectionUrl);
if (err) { if (redirectOrUndefined) {
await dispatch(authenticateFailure(err.message)); window.location.href = redirectOrUndefined.redirect;
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'];
return; return;
} }
dispatch(authenticateSuccess());
dispatch(setUsername(''));
dispatch(setPassword(''));
// fetch state to move to next stage in case redirect is not possible // fetch state to move to next stage in case redirect is not possible
await FetchStateBehavior(dispatch); await FetchStateBehavior(dispatch);
} else if (res.status === 204) { } catch (err) {
dispatch(authenticateSuccess()); dispatch(setPassword(''));
dispatch(authenticateFailure(err.message));
// fetch state to move to next stage
await FetchStateBehavior(dispatch);
} else {
dispatch(authenticateFailure('Unknown error'));
throw new Error('Unknown error... (' + res.status + ')');
} }
} }
} }
const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => { const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
return { return {
onUsernameChanged: function(username: string) {
dispatch(setUsername(username));
},
onPasswordChanged: function(password: string) {
dispatch(setPassword(password));
},
onAuthenticationRequested: onAuthenticationRequested(dispatch, ownProps.redirectionUrl), onAuthenticationRequested: onAuthenticationRequested(dispatch, ownProps.redirectionUrl),
} }
} }

View File

@ -4,7 +4,6 @@ import { Dispatch } from 'redux';
import SecondFactorDuoPush, { StateProps, OwnProps, DispatchProps } from '../../../components/SecondFactorDuoPush/SecondFactorDuoPush'; import SecondFactorDuoPush, { StateProps, OwnProps, DispatchProps } from '../../../components/SecondFactorDuoPush/SecondFactorDuoPush';
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior'; import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
import TriggerDuoPushAuth from '../../../behaviors/TriggerDuoPushAuth'; import TriggerDuoPushAuth from '../../../behaviors/TriggerDuoPushAuth';
import RedirectionResponse from '../../../services/RedirectResponse';
const mapStateToProps = (state: RootState): StateProps => ({ const mapStateToProps = (state: RootState): StateProps => ({
@ -20,7 +19,7 @@ async function redirectIfPossible(body: any) {
return false; 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() { async function handle() {
const redirected = await redirectIfPossible(body); const redirected = await redirectIfPossible(body);
if (!redirected) { if (!redirected) {

View File

@ -7,7 +7,6 @@ import {
oneTimePasswordVerificationFailure, oneTimePasswordVerificationFailure,
oneTimePasswordVerificationSuccess oneTimePasswordVerificationSuccess
} from '../../../reducers/Portal/SecondFactor/actions'; } from '../../../reducers/Portal/SecondFactor/actions';
import to from 'await-to-js';
import AutheliaService from '../../../services/AutheliaService'; import AutheliaService from '../../../services/AutheliaService';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import FetchStateBehavior from '../../../behaviors/FetchStateBehavior'; import FetchStateBehavior from '../../../behaviors/FetchStateBehavior';
@ -18,21 +17,6 @@ const mapStateToProps = (state: RootState): StateProps => ({
oneTimePasswordVerificationError: state.secondFactor.oneTimePasswordVerificationError, 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 handleSuccess(dispatch: Dispatch, duration?: number) {
async function handle() { async function handle() {
await FetchStateBehavior(dispatch); await FetchStateBehavior(dispatch);
@ -48,23 +32,17 @@ async function handleSuccess(dispatch: Dispatch, duration?: number) {
const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => { const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
return { return {
onOneTimePasswordValidationRequested: async (token: string) => { 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 { try {
await redirectIfPossible(dispatch, res); dispatch(oneTimePasswordVerification());
const response = await AutheliaService.verifyTotpToken(token, ownProps.redirectionUrl);
dispatch(oneTimePasswordVerificationSuccess()); dispatch(oneTimePasswordVerificationSuccess());
if (response) {
window.location.href = response.redirect;
return;
}
await handleSuccess(dispatch); await handleSuccess(dispatch);
} catch (err) { } catch (err) {
console.error(err);
dispatch(oneTimePasswordVerificationFailure(err.message)); dispatch(oneTimePasswordVerificationFailure(err.message));
} }
}, },

View File

@ -5,7 +5,6 @@ import SecondFactorU2F, { StateProps, OwnProps } from '../../../components/Secon
import AutheliaService from '../../../services/AutheliaService'; import AutheliaService from '../../../services/AutheliaService';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import u2fApi from 'u2f-api'; import u2fApi from 'u2f-api';
import to from 'await-to-js';
import { import {
securityKeySignSuccess, securityKeySignSuccess,
securityKeySign, securityKeySign,
@ -20,58 +19,27 @@ const mapStateToProps = (state: RootState): StateProps => ({
}); });
async function triggerSecurityKeySigning(dispatch: Dispatch, redirectionUrl: string | null) { async function triggerSecurityKeySigning(dispatch: Dispatch, redirectionUrl: string | null) {
let err, result;
dispatch(securityKeySign()); dispatch(securityKeySign());
[err, result] = await to(AutheliaService.requestSigning()); const signRequest = await AutheliaService.requestSigning();
if (err) { const signRequests: u2fApi.SignRequest[] = [];
await dispatch(securityKeySignFailure(err.message)); for (var i in signRequest.registeredKeys) {
throw err; 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) { if (response) {
await dispatch(securityKeySignFailure('No response')); window.location.href = response.redirect;
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'];
return; return;
} }
return; await handleSuccess(dispatch, 1000);
} }
async function handleSuccess(dispatch: Dispatch, duration?: number) { async function handleSuccess(dispatch: Dispatch, duration?: number) {
@ -93,7 +61,12 @@ const mapDispatchToProps = (dispatch: Dispatch, ownProps: OwnProps) => {
await dispatch(push('/confirmation-sent')); await dispatch(push('/confirmation-sent'));
}, },
onInit: async () => { onInit: async () => {
await triggerSecurityKeySigning(dispatch, ownProps.redirectionUrl); try {
await triggerSecurityKeySigning(dispatch, ownProps.redirectionUrl);
} catch (err) {
console.error(err);
await dispatch(securityKeySignFailure(err.message));
}
}, },
} }
} }

View File

@ -27,6 +27,8 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
const params = QueryString.parse(ownProps.location.search); const params = QueryString.parse(ownProps.location.search);
if ('rd' in params) { if ('rd' in params) {
url = params['rd'] as string; url = params['rd'] as string;
} else if (state.authentication.remoteState && state.authentication.remoteState.default_redirection_url) {
url = state.authentication.remoteState.default_redirection_url;
} }
} }

View File

@ -2,48 +2,23 @@ import { connect } from 'react-redux';
import OneTimePasswordRegistrationView from '../../../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView'; import OneTimePasswordRegistrationView from '../../../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView';
import { RootState } from '../../../reducers'; import { RootState } from '../../../reducers';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import {to} from 'await-to-js';
import { generateTotpSecret, generateTotpSecretSuccess, generateTotpSecretFailure } from '../../../reducers/Portal/OneTimePasswordRegistration/actions'; import { generateTotpSecret, generateTotpSecretSuccess, generateTotpSecretFailure } from '../../../reducers/Portal/OneTimePasswordRegistration/actions';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import AutheliaService from '../../../services/AutheliaService';
const mapStateToProps = (state: RootState) => ({ const mapStateToProps = (state: RootState) => ({
error: state.oneTimePasswordRegistration.error, error: state.oneTimePasswordRegistration.error,
secret: state.oneTimePasswordRegistration.secret, 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) { async function tryGenerateTotpSecret(dispatch: Dispatch, token: string) {
let err, result; try {
dispatch(generateTotpSecret()); dispatch(generateTotpSecret());
[err, result] = await to(checkIdentity(token)); const res = await AutheliaService.completeOneTimePasswordRegistrationIdentityValidation(token);
if (err) { dispatch(generateTotpSecretSuccess(res));
const e = err; } catch (err) {
setTimeout(() => { dispatch(generateTotpSecretFailure(err.message));
dispatch(generateTotpSecretFailure(e.message));
}, 2000);
return;
} }
dispatch(generateTotpSecretSuccess(result));
} }
const mapDispatchToProps = (dispatch: Dispatch) => { const mapDispatchToProps = (dispatch: Dispatch) => {

View File

@ -12,11 +12,19 @@ const mapStateToProps = (state: RootState): StateProps => ({
const mapDispatchToProps = (dispatch: Dispatch) => { const mapDispatchToProps = (dispatch: Dispatch) => {
return { return {
onInit: async (token: string) => { onInit: async (token: string) => {
await AutheliaService.completePasswordResetIdentityValidation(token); try {
await AutheliaService.completePasswordResetIdentityValidation(token);
} catch (err) {
console.error(err);
}
}, },
onPasswordResetRequested: async (newPassword: string) => { onPasswordResetRequested: async (newPassword: string) => {
await AutheliaService.resetPassword(newPassword); try {
await dispatch(push('/')); await AutheliaService.resetPassword(newPassword);
await dispatch(push('/'));
} catch (err) {
console.error(err);
}
}, },
onCancelClicked: async () => { onCancelClicked: async () => {
await dispatch(push('/')); await dispatch(push('/'));

View File

@ -12,26 +12,30 @@ const mapStateToProps = (state: RootState) => ({
error: state.securityKeyRegistration.error, error: state.securityKeyRegistration.error,
}); });
function fail(dispatch: Dispatch, err: Error) {
console.error(err);
dispatch(registerSecurityKeyFailure(err.message));
}
const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => { const mapDispatchToProps = (dispatch: Dispatch, ownProps: Props) => {
return { return {
onInit: async (token: string) => { onInit: async (token: string) => {
try { try {
dispatch(registerSecurityKey()); dispatch(registerSecurityKey());
await AutheliaService.completeSecurityKeyRegistrationIdentityValidation(token); const registerRequest = await AutheliaService.completeSecurityKeyRegistrationIdentityValidation(token);
const registerRequest = await AutheliaService.requestSecurityKeyRegistration(); const registerRequests: U2fApi.RegisterRequest[] = [];
const registerResponse = await U2fApi.register([registerRequest], [], 60); 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); await AutheliaService.completeSecurityKeyRegistration(registerResponse);
dispatch(registerSecurityKeySuccess()); dispatch(registerSecurityKeySuccess());
setTimeout(() => { setTimeout(() => {
ownProps.history.push('/'); ownProps.history.push('/');
}, 2000); }, 2000);
} catch(err) { } catch(err) {
fail(dispatch, err); console.error(err);
dispatch(registerSecurityKeyFailure(err.message));
} }
}, },
onBackClicked: () => { onBackClicked: () => {

View File

@ -2,7 +2,9 @@ import { createAction } from 'typesafe-actions';
import { import {
AUTHENTICATE_REQUEST, AUTHENTICATE_REQUEST,
AUTHENTICATE_SUCCESS, AUTHENTICATE_SUCCESS,
AUTHENTICATE_FAILURE AUTHENTICATE_FAILURE,
FIRST_FACTOR_SET_USERNAME,
FIRST_FACTOR_SET_PASSWORD
} from "../../constants"; } from "../../constants";
/* AUTHENTICATE_REQUEST */ /* AUTHENTICATE_REQUEST */
@ -11,3 +13,11 @@ export const authenticateSuccess = createAction(AUTHENTICATE_SUCCESS);
export const authenticateFailure = createAction(AUTHENTICATE_FAILURE, resolve => { export const authenticateFailure = createAction(AUTHENTICATE_FAILURE, resolve => {
return (error: string) => resolve(error); 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);
});

View File

@ -14,12 +14,16 @@ interface FirstFactorState {
lastResult: Result; lastResult: Result;
loading: boolean; loading: boolean;
error: string | null; error: string | null;
username: string;
password: string;
} }
const firstFactorInitialState: FirstFactorState = { const firstFactorInitialState: FirstFactorState = {
lastResult: Result.NONE, lastResult: Result.NONE,
loading: false, loading: false,
error: null, error: null,
username: '',
password: '',
} }
export default (state = firstFactorInitialState, action: FirstFactorAction): FirstFactorState => { export default (state = firstFactorInitialState, action: FirstFactorAction): FirstFactorState => {
@ -44,6 +48,16 @@ export default (state = firstFactorInitialState, action: FirstFactorAction): Fir
loading: false, loading: false,
error: action.payload, error: action.payload,
}; };
case getType(Actions.setUsername):
return {
...state,
username: action.payload,
}
case getType(Actions.setPassword):
return {
...state,
password: action.payload,
}
} }
return state; return state;
} }

View File

@ -4,9 +4,12 @@ export const FETCH_STATE_SUCCESS = '@portal/fetch_state_success';
export const FETCH_STATE_FAILURE = '@portal/fetch_state_failure'; export const FETCH_STATE_FAILURE = '@portal/fetch_state_failure';
// AUTHENTICATION PROCESS // AUTHENTICATION PROCESS
export const AUTHENTICATE_REQUEST = '@portal/authenticate_request'; export const FIRST_FACTOR_SET_USERNAME = "@portal/first_factor/set_username";
export const AUTHENTICATE_SUCCESS = '@portal/authenticate_success'; export const FIRST_FACTOR_SET_PASSWORD = "@portal/first_factor/set_password";
export const AUTHENTICATE_FAILURE = '@portal/authenticate_failure';
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 // SECOND FACTOR PAGE
export const SET_SECURITY_KEY_SUPPORTED = '@portal/second_factor/set_security_key_supported'; export const SET_SECURITY_KEY_SUPPORTED = '@portal/second_factor/set_security_key_supported';

View File

@ -4,6 +4,7 @@ import SecurityKeyRegistrationView from "../containers/views/SecurityKeyRegistra
import ForgotPasswordView from "../containers/views/ForgotPasswordView/ForgotPasswordView"; import ForgotPasswordView from "../containers/views/ForgotPasswordView/ForgotPasswordView";
import ResetPasswordView from "../containers/views/ResetPasswordView/ResetPasswordView"; import ResetPasswordView from "../containers/views/ResetPasswordView/ResetPasswordView";
import AuthenticationView from "../containers/views/AuthenticationView/AuthenticationView"; import AuthenticationView from "../containers/views/AuthenticationView/AuthenticationView";
import LogoutView from "../views/LogoutView/LogoutView";
export const routes = [{ export const routes = [{
path: '/', path: '/',
@ -29,4 +30,8 @@ export const routes = [{
path: '/reset-password', path: '/reset-password',
title: 'Reset password', title: 'Reset password',
component: ResetPasswordView, component: ResetPasswordView,
}, {
path: '/logout',
title: 'Logout',
component: LogoutView,
}] }]

View File

@ -1,58 +1,73 @@
import RemoteState from "../views/AuthenticationView/RemoteState"; import RemoteState from "../views/AuthenticationView/RemoteState";
import U2fApi, { SignRequest } from "u2f-api"; import U2fApi from "u2f-api";
import Method2FA from "../types/Method2FA"; import Method2FA from "../types/Method2FA";
import RedirectResponse from "./RedirectResponse"; import { string } from "prop-types";
import PreferedMethodResponse from "./PreferedMethodResponse";
interface DataResponse<T> {
status: "OK";
data: T;
}
interface ErrorResponse {
status: "KO";
message: string;
}
type ServiceResponse<T> = DataResponse<T> | ErrorResponse;
class AutheliaService { 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> { static async fetchSafeJson<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(url, options); const res = await fetch(url, options);
if (res.status !== 200) { if (res.status !== 200) {
throw new Error('Status code ' + res.status); 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. * Fetch current authentication state.
*/ */
static async fetchState(): Promise<RemoteState> { 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, static async postFirstFactorAuth(username: string, password: string,
rememberMe: boolean, redirectionUrl: string | null) { rememberMe: boolean, targetURL: string | null) {
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
if (redirectionUrl) { const requestBody: {
headers['X-Target-Url'] = redirectionUrl; 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', method: 'POST',
headers: headers, headers: headers,
body: JSON.stringify({ body: JSON.stringify(requestBody)
username: username,
password: password,
keepMeLoggedIn: rememberMe,
})
}); });
} }
static async postLogout() { static async postLogout() {
return this.fetchSafe('/api/logout', { return this.fetchSafeJson<undefined>('/api/logout', {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -62,81 +77,81 @@ class AutheliaService {
} }
static async startU2FRegistrationIdentityProcess() { static async startU2FRegistrationIdentityProcess() {
return this.fetchSafe('/api/secondfactor/u2f/identity/start', { return this.fetchSafeJson<undefined>('/api/secondfactor/u2f/identity/start', {
method: 'POST', method: 'POST',
}); });
} }
static async startTOTPRegistrationIdentityProcess() { static async startTOTPRegistrationIdentityProcess() {
return this.fetchSafe('/api/secondfactor/totp/identity/start', { return this.fetchSafeJson<undefined>('/api/secondfactor/totp/identity/start', {
method: 'POST', method: 'POST',
}); });
} }
static async requestSigning() { 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( static async completeSecurityKeySigning(
response: U2fApi.SignResponse, redirectionUrl: string | null) { response: U2fApi.SignResponse, targetURL: string | null) {
const headers: Record<string, string> = { const headers: Record<string, string> = {'Content-Type': 'application/json',}
'Accept': 'application/json', const requestBody: {signResponse: U2fApi.SignResponse, targetURL?: string} = {
'Content-Type': 'application/json', signResponse: response,
};
if (targetURL) {
requestBody.targetURL = targetURL;
} }
if (redirectionUrl) { return this.fetchSafeJson<{redirect: string}|undefined>('/api/secondfactor/u2f/sign', {
headers['X-Target-Url'] = redirectionUrl;
}
return this.fetchSafe('/api/u2f/sign', {
method: 'POST', method: 'POST',
headers: headers, headers: headers,
body: JSON.stringify(response), body: JSON.stringify(requestBody),
}); });
} }
static async verifyTotpToken( static async verifyTotpToken(
token: string, redirectionUrl: string | null) { token: string, targetURL: string | null) {
const headers: Record<string, string> = {
const headers: Record<string, string> = {
'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
if (redirectionUrl) { var requestBody: {token: string, targetURL?: string} = {token};
headers['X-Target-Url'] = redirectionUrl; if (targetURL) {
requestBody.targetURL = targetURL;
} }
return this.fetchSafe('/api/totp', { return this.fetchSafeJson<{redirect: string}|undefined>('/api/secondfactor/totp', {
method: 'POST', method: 'POST',
headers: headers, 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> = { const headers: Record<string, string> = {
'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
if (redirectionUrl) { const requestBody: {targetURL?: string} = {}
headers['X-Target-Url'] = redirectionUrl; if (targetURL) {
requestBody.targetURL = targetURL;
} }
const res = await this.fetchSafe('/api/duo-push', { return this.fetchSafeJson<{redirect: string}|undefined>('/api/secondfactor/duo', {
method: 'POST', method: 'POST',
headers: headers, 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) { static async initiatePasswordResetIdentityValidation(username: string) {
return this.fetchSafe('/api/password-reset/identity/start', { return this.fetchSafeJson<undefined>('/api/reset-password/identity/start', {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -147,13 +162,17 @@ class AutheliaService {
} }
static async completePasswordResetIdentityValidation(token: string) { 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', method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({token})
}); });
} }
static async resetPassword(newPassword: string) { static async resetPassword(newPassword: string) {
return this.fetchSafe('/api/password-reset', { return this.fetchSafeJson<undefined>('/api/reset-password', {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -164,27 +183,14 @@ class AutheliaService {
} }
static async fetchPrefered2faMethod(): Promise<Method2FA> { static async fetchPrefered2faMethod(): Promise<Method2FA> {
const doc = await this.fetchSafeJson<PreferedMethodResponse>('/api/secondfactor/preferences'); const res = await this.fetchSafeJson<{method: Method2FA}>('/api/secondfactor/preferences');
if (!doc) { return res.method;
throw new Error("No response.");
}
if (doc.error) {
throw new Error(doc.error);
}
if (!doc.method) {
throw new Error("No method.");
}
return doc.method;
} }
static async setPrefered2faMethod(method: Method2FA): Promise<void> { static async setPrefered2faMethod(method: Method2FA): Promise<void> {
await this.fetchSafe('/api/secondfactor/preferences', { return this.fetchSafeJson<undefined>('/api/secondfactor/preferences', {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({method}) body: JSON.stringify({method})
@ -192,11 +198,12 @@ class AutheliaService {
} }
static async getAvailable2faMethods(): Promise<Method2FA[]> { 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> { static async completeSecurityKeyRegistration(
return await this.fetchSafe('/api/u2f/register', { response: U2fApi.RegisterResponse): Promise<undefined> {
return this.fetchSafeJson('/api/secondfactor/u2f/register', {
method: 'POST', method: 'POST',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
@ -206,19 +213,30 @@ class AutheliaService {
}); });
} }
static async requestSecurityKeyRegistration() { static async completeSecurityKeyRegistrationIdentityValidation(token: string) {
return this.fetchSafeJson<U2fApi.RegisterRequest>('/api/u2f/register_request') 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) { static async completeOneTimePasswordRegistrationIdentityValidation(token: string) {
const res = await this.fetchSafeJson(`/api/secondfactor/u2f/identity/finish?token=${token}`, { return this.fetchSafeJson<{base32_secret: string, otpauth_url: string}>(`/api/secondfactor/totp/identity/finish`, {
method: 'POST', method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({token})
}); });
if ('error' in res) {
throw new Error(res['error']);
}
return res;
} }
} }

View File

@ -1,6 +0,0 @@
export default interface RedirectResponse {
redirect?: string;
error?: string;
}

View 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='/' />;
}
}

View File

@ -10,6 +10,10 @@ port: 9091
# Level of verbosity for logs # Level of verbosity for logs
logs_level: debug 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 # Default redirection URL
# #
# If user tries to authenticate without any referer, Authelia # If user tries to authenticate without any referer, Authelia
@ -263,19 +267,20 @@ notifier:
## filesystem: ## filesystem:
## filename: /tmp/authelia/notification.txt ## filename: /tmp/authelia/notification.txt
# Use your email account to send the notifications. You can use an app password. # Use a SMTP server for sending notifications. Authelia uses PLAIN method to authenticate.
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/ # [Security] Make sure the connection is made over TLS otherwise your password will transit in plain text.
## email:
## username: user@example.com
## password: yourpassword
## sender: admin@example.com
## service: gmail
# Use a SMTP server for sending notifications
smtp: smtp:
username: test username: test
password: password password: password
secure: false
host: 127.0.0.1 host: 127.0.0.1
port: 1025 port: 1025
sender: admin@example.com 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
View 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
}

View 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)
}

View 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'"))
}
}

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View 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,
}

View 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"`
}

View File

@ -0,0 +1,6 @@
package schema
// TOTPConfiguration represents the configuration related to TOTP options.
type TOTPConfiguration struct {
Issuer string
}

View 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
}

View 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)
}

View 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)
}
}

View 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))
}

View 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)
}
}

View 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)
}

View 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"))
}
}

View 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")
}

View 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
}
}

View File

@ -14,7 +14,7 @@ Here is a commented example of configuration
set $upstream_verify https://authelia.example.com/api/verify; set $upstream_verify https://authelia.example.com/api/verify;
set $upstream_endpoint http://nginx-backend; 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; ssl_certificate_key /etc/ssl/server.key;
# Use HSTS, please beware of what you're doing if you set it. # Use HSTS, please beware of what you're doing if you set it.

32
duo/duo.go Normal file
View 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
View 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"`
}

View File

@ -15,7 +15,7 @@ http {
resolver 127.0.0.11 ipv6=off; 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; ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

View 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-----

View File

@ -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-----

View File

@ -1,11 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhq
GUNYRu12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/
EtN7WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwID
AQABoAAwDQYJKoZIhvcNAQELBQADgYEAmCX60kspIw1Zfb79AQOarFW5Q2K2h5Vx
/cRbDyHlKtbmG77EtICccULyqf76B1gNRw5Zq3lSotSUcLzsWcdesXCFDC7k87Qf
mpQKPj6GdTYJvdWf8aDwt32tAqWuBIRoAbdx5WbFPPWVfDcm7zDJefBrhNUDH0Qd
vcnxjvPMmOM=
-----END CERTIFICATE REQUEST-----

View File

@ -1,15 +1,27 @@
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhqGUNY MIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S
Ru12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/EtN7 KcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X
WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwIDAQAB yEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M
AoGBAIwGcfkO30UawJ+daDeF4g5ejI/toM+NYWuiwBNbWJoQl+Bj1o+gt4obvxKq lqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE
tKNX7OxelepZ4oZB0CIuf2LHQfU6cVGdu//or7nfS2FLBYStopZyL6KorZbkqsj1 lgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R
ikQN4GosJQqaYkexnwjItMFaHaRRX6YnIXp42Jl1glitO3+5AkEA+thn/vwFo24I cMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn
fC+7ORpmLi+BVAkTuhMm+C6TIV6s64B+A5oQ82OBCYK9YCOWmS6JHHFDrxJla+3M V3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv
2U9KXky63wJBANHQCFCirfuT6esSjbqpCeqtmZG5LWHtL12V9DF7yjHPjmHL9uRu B7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd
e9W+Uz33IJbqd82gtZ/ARfpYEjD0JEieQTUCQFo872xzDTQ1qSfDo/5u2MNUo5mv zV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036
ikEuEp7FYnhmrp4poyt4iRCFgy4Ask+bfdmtO/XXaRnZ7FJfQYoLVB2ITNECQQCN UxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1
gOiauZztl4yj5heAVJFDnWF9To61BOp1C7VtyjdL8NfuTUluNrV+KqapnAp2vhue /P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI
q0zTOTH47X0XVxFBiLohAkBuQzPey5I3Ui8inE4sDt/fqX8r/GMhBTxIb9KlV/H6 F4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd
jKZNs/83n5/ohaX36er8svW9PB4pcqENZ+kBpvDtKVwS 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----- -----END RSA PRIVATE KEY-----

View File

@ -18,7 +18,7 @@ http {
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $backend_endpoint <%= authelia_backend %>; set $backend_endpoint <%= authelia_backend %>;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key; ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -26,7 +26,7 @@ http {
# Serves the portal application. # Serves the portal application.
location / { location / {
proxy_pass $backend_endpoint/index.html; proxy_pass $backend_endpoint;
} }
location /static { location /static {
@ -62,7 +62,7 @@ http {
set $frontend_endpoint http://192.168.240.1:3000; set $frontend_endpoint http://192.168.240.1:3000;
set $backend_endpoint <%= authelia_backend %>; set $backend_endpoint <%= authelia_backend %>;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key; ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -108,7 +108,7 @@ http {
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://nginx-backend; 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; ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -135,7 +135,7 @@ http {
set $upstream_endpoint http://nginx-backend; set $upstream_endpoint http://nginx-backend;
set $upstream_headers http://httpbin:8000/headers; 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; ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -179,7 +179,7 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
# Provide either X-Original-URL and X-Forwarded-Proto or # 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. # 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. # 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-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host; 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; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -227,7 +227,7 @@ http {
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://smtp:1080; 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; ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -247,7 +247,7 @@ http {
resolver 127.0.0.11 ipv6=off; resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://duo-api:3000; 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; ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -264,7 +264,7 @@ http {
listen 8080 ssl; listen 8080 ssl;
server_name _; server_name _;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key; ssl_certificate_key /etc/ssl/server.key;
return 301 https://home.example.com:8080/; return 301 https://home.example.com:8080/;

View 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-----

View File

@ -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-----

View File

@ -1,11 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBhDCB7gIBADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh
MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEB
AQUAA4GNADCBiQKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhq
GUNYRu12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/
EtN7WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwID
AQABoAAwDQYJKoZIhvcNAQELBQADgYEAmCX60kspIw1Zfb79AQOarFW5Q2K2h5Vx
/cRbDyHlKtbmG77EtICccULyqf76B1gNRw5Zq3lSotSUcLzsWcdesXCFDC7k87Qf
mpQKPj6GdTYJvdWf8aDwt32tAqWuBIRoAbdx5WbFPPWVfDcm7zDJefBrhNUDH0Qd
vcnxjvPMmOM=
-----END CERTIFICATE REQUEST-----

View File

@ -1,15 +1,27 @@
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhqGUNY MIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S
Ru12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/EtN7 KcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X
WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwIDAQAB yEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M
AoGBAIwGcfkO30UawJ+daDeF4g5ejI/toM+NYWuiwBNbWJoQl+Bj1o+gt4obvxKq lqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE
tKNX7OxelepZ4oZB0CIuf2LHQfU6cVGdu//or7nfS2FLBYStopZyL6KorZbkqsj1 lgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R
ikQN4GosJQqaYkexnwjItMFaHaRRX6YnIXp42Jl1glitO3+5AkEA+thn/vwFo24I cMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn
fC+7ORpmLi+BVAkTuhMm+C6TIV6s64B+A5oQ82OBCYK9YCOWmS6JHHFDrxJla+3M V3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv
2U9KXky63wJBANHQCFCirfuT6esSjbqpCeqtmZG5LWHtL12V9DF7yjHPjmHL9uRu B7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd
e9W+Uz33IJbqd82gtZ/ARfpYEjD0JEieQTUCQFo872xzDTQ1qSfDo/5u2MNUo5mv zV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036
ikEuEp7FYnhmrp4poyt4iRCFgy4Ask+bfdmtO/XXaRnZ7FJfQYoLVB2ITNECQQCN UxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1
gOiauZztl4yj5heAVJFDnWF9To61BOp1C7VtyjdL8NfuTUluNrV+KqapnAp2vhue /P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI
q0zTOTH47X0XVxFBiLohAkBuQzPey5I3Ui8inE4sDt/fqX8r/GMhBTxIb9KlV/H6 F4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd
jKZNs/83n5/ohaX36er8svW9PB4pcqENZ+kBpvDtKVwS 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----- -----END RSA PRIVATE KEY-----

View 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-----

View 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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -10,6 +10,8 @@ port: 80
# Level of verbosity for logs # Level of verbosity for logs
logs_level: debug logs_level: debug
jwt_secret: an_unsecure_secret
# Default redirection URL # Default redirection URL
# #
# If user tries to authenticate without any referer, Authelia # If user tries to authenticate without any referer, Authelia
@ -35,7 +37,7 @@ authentication_backend:
# production. # production.
ldap: ldap:
# The url of the ldap server # The url of the ldap server
url: ldap://ldap-service url: ldap-service:389
# The base dn for every entries # The base dn for every entries
base_dn: dc=example,dc=com base_dn: dc=example,dc=com
@ -46,7 +48,7 @@ authentication_backend:
# The users filter used to find the user DN # The users filter used to find the user DN
# {0} is a matcher replaced by username. # {0} is a matcher replaced by username.
# 'cn={0}' by default. # 'cn={0}' by default.
users_filter: cn={0} users_filter: (cn={0})
# An additional dn to define the scope of groups # An additional dn to define the scope of groups
additional_groups_dn: ou=groups additional_groups_dn: ou=groups
@ -195,20 +197,9 @@ notifier:
# For testing purpose, notifications can be sent in a file # For testing purpose, notifications can be sent in a file
# filesystem: # filesystem:
# filename: /tmp/authelia/notification.txt # 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 # Use a SMTP server for sending notifications
smtp: smtp:
username: test
password: password
secure: false
host: 'mailcatcher-service' host: 'mailcatcher-service'
port: 1025 port: 1025
sender: admin@example.com sender: admin@example.com

View File

@ -2,7 +2,7 @@
start_apps() { start_apps() {
# Create TLS certificate and key for HTTPS termination # 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 # Spawn the applications
kubectl apply -f apps kubectl apply -f apps
@ -13,7 +13,11 @@ start_ingress_controller() {
} }
start_dashboard() { 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 # Spawn Redis and Mongo as backend for Authelia
@ -28,6 +32,7 @@ start_mail() {
} }
start_ldap() { start_ldap() {
kubectl create configmap ldap-config --namespace=authelia --from-file=ldap/base.ldif --from-file=ldap/access.rules
kubectl apply -f ldap kubectl apply -f ldap
} }

View 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

View File

@ -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

View File

@ -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

View 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

View 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/

View File

@ -21,3 +21,30 @@ spec:
image: clems4ever/authelia-test-ldap image: clems4ever/authelia-test-ldap
ports: ports:
- containerPort: 389 - 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
View 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
View 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")

View 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)
}

View 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)
}

View 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()
}

View 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)
}

View 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()
}
}

View 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)
}

View 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()
}

View 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)
}

View 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)

View 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)

View 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()
}

View 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)

View 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()
}

View 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