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.
pull/419/head
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
.kube
.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
node_js:
- '9'
go:
- '1.13'
services:
- docker
- ntp
@ -12,9 +12,11 @@ addons:
sources:
- google-chrome
packages:
- xvfb
- libgif-dev
- google-chrome-stable
before_script:
- curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
- nvm install v9 && nvm use v9 && npm i
script:
- "./scripts/authelia-scripts travis"
after_success:

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
Docker image but pick a version instead and check this file before upgrading. This is where you will get information about breaking changes and about what you should do to overcome those changes.
## Breaking in v4.0.0
Authelia has been rewritten in Go for better performance and reliability.
### Model of U2F devices in MongoDB
The model of U2F devices stored in MongoDB has been updated to better fit with the Go library handling U2F keys.
### Removal of flag secure for SMTP notifier
The go library for sending e-mails automatically switch to TLS if possible according to https://golang.org/pkg/net/smtp/#SendMail.
## Breaking in v3.14.0
### Headers in nginx configuration

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 \
.build-deps make g++ python && \
npm install --production && \
apk del .build-deps
# Install the libc required by the password hashing compiled with CGO.
RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk
RUN apk --no-cache add glibc-2.30-r0.apk
COPY dist/server /usr/src/server
ADD dist/authelia authelia
ADD dist/public_html public_html
EXPOSE 9091
VOLUME /etc/authelia
VOLUME /var/lib/authelia
CMD ["node", "server/src/index.js", "/etc/authelia/config.yml"]
CMD ["./authelia", "-config", "/etc/authelia/config.yml"]

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

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
}

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

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"
echo "[BOOTSTRAP] Installing npm packages..."
npm i
pushd client
@ -27,5 +26,8 @@ fi
echo "[BOOTSTRAP] Running additional bootstrap steps..."
authelia-scripts bootstrap
# Create temporary directory that will contain the databases used in tests.
mkdir -p /tmp/authelia
echo "[BOOTSTRAP] Run 'authelia-scripts suites start dockerhub' to start Authelia and visit https://home.example.com:8080."
echo "[BOOTSTRAP] More details at https://github.com/clems4ever/authelia/blob/master/docs/getting-started.md"

7538
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,48 +2,23 @@ import { connect } from 'react-redux';
import OneTimePasswordRegistrationView from '../../../views/OneTimePasswordRegistrationView/OneTimePasswordRegistrationView';
import { RootState } from '../../../reducers';
import { Dispatch } from 'redux';
import {to} from 'await-to-js';
import { generateTotpSecret, generateTotpSecretSuccess, generateTotpSecretFailure } from '../../../reducers/Portal/OneTimePasswordRegistration/actions';
import { push } from 'connected-react-router';
import AutheliaService from '../../../services/AutheliaService';
const mapStateToProps = (state: RootState) => ({
error: state.oneTimePasswordRegistration.error,
secret: state.oneTimePasswordRegistration.secret,
});
async function checkIdentity(token: string) {
return fetch(`/api/secondfactor/totp/identity/finish?token=${token}`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
})
.then(async (res) => {
if (res.status !== 200) {
throw new Error('Status code ' + res.status);
}
const body = await res.json();
if ('error' in body) {
throw new Error(body['error']);
}
return body;
});
}
async function tryGenerateTotpSecret(dispatch: Dispatch, token: string) {
let err, result;
dispatch(generateTotpSecret());
[err, result] = await to(checkIdentity(token));
if (err) {
const e = err;
setTimeout(() => {
dispatch(generateTotpSecretFailure(e.message));
}, 2000);
return;
try {
dispatch(generateTotpSecret());
const res = await AutheliaService.completeOneTimePasswordRegistrationIdentityValidation(token);
dispatch(generateTotpSecretSuccess(res));
} catch (err) {
dispatch(generateTotpSecretFailure(err.message));
}
dispatch(generateTotpSecretSuccess(result));
}
const mapDispatchToProps = (dispatch: Dispatch) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
logs_level: debug
# The secret used to generate JWT tokens when validating user identity by
# email confirmation.
jwt_secret: a_very_important_secret
# Default redirection URL
#
# If user tries to authenticate without any referer, Authelia
@ -263,19 +267,20 @@ notifier:
## filesystem:
## filename: /tmp/authelia/notification.txt
# Use your email account to send the notifications. You can use an app password.
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/
## email:
## username: user@example.com
## password: yourpassword
## sender: admin@example.com
## service: gmail
# Use a SMTP server for sending notifications
# Use a SMTP server for sending notifications. Authelia uses PLAIN method to authenticate.
# [Security] Make sure the connection is made over TLS otherwise your password will transit in plain text.
smtp:
username: test
password: password
secure: false
host: 127.0.0.1
port: 1025
sender: admin@example.com
# Sending an email using a Gmail account is as simple as the next section.
# You need to create an app password by following: https://support.google.com/accounts/answer/185833?hl=en
## smtp:
## username: myaccount@gmail.com
## password: yourapppassword
## sender: admin@example.com
## host: smtp.gmail.com
## port: 587

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_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key;
# Use HSTS, please beware of what you're doing if you set it.

32
duo/duo.go 100644
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 100644
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;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

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-----
MIICXQIBAAKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhqGUNY
Ru12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/EtN7
WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwIDAQAB
AoGBAIwGcfkO30UawJ+daDeF4g5ejI/toM+NYWuiwBNbWJoQl+Bj1o+gt4obvxKq
tKNX7OxelepZ4oZB0CIuf2LHQfU6cVGdu//or7nfS2FLBYStopZyL6KorZbkqsj1
ikQN4GosJQqaYkexnwjItMFaHaRRX6YnIXp42Jl1glitO3+5AkEA+thn/vwFo24I
fC+7ORpmLi+BVAkTuhMm+C6TIV6s64B+A5oQ82OBCYK9YCOWmS6JHHFDrxJla+3M
2U9KXky63wJBANHQCFCirfuT6esSjbqpCeqtmZG5LWHtL12V9DF7yjHPjmHL9uRu
e9W+Uz33IJbqd82gtZ/ARfpYEjD0JEieQTUCQFo872xzDTQ1qSfDo/5u2MNUo5mv
ikEuEp7FYnhmrp4poyt4iRCFgy4Ask+bfdmtO/XXaRnZ7FJfQYoLVB2ITNECQQCN
gOiauZztl4yj5heAVJFDnWF9To61BOp1C7VtyjdL8NfuTUluNrV+KqapnAp2vhue
q0zTOTH47X0XVxFBiLohAkBuQzPey5I3Ui8inE4sDt/fqX8r/GMhBTxIb9KlV/H6
jKZNs/83n5/ohaX36er8svW9PB4pcqENZ+kBpvDtKVwS
MIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S
KcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X
yEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M
lqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE
lgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R
cMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn
V3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv
B7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd
zV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036
UxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1
/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI
F4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd
7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs
hcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA
06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh
IlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75
HmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/
rW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE
Zrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b
bx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq
0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS
qfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2
qSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L
zqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2
HEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==
-----END RSA PRIVATE KEY-----

View File

@ -18,7 +18,7 @@ http {
resolver 127.0.0.11 ipv6=off;
set $backend_endpoint <%= authelia_backend %>;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -26,7 +26,7 @@ http {
# Serves the portal application.
location / {
proxy_pass $backend_endpoint/index.html;
proxy_pass $backend_endpoint;
}
location /static {
@ -62,7 +62,7 @@ http {
set $frontend_endpoint http://192.168.240.1:3000;
set $backend_endpoint <%= authelia_backend %>;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -108,7 +108,7 @@ http {
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://nginx-backend;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -135,7 +135,7 @@ http {
set $upstream_endpoint http://nginx-backend;
set $upstream_headers http://httpbin:8000/headers;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -179,7 +179,7 @@ http {
proxy_set_header X-Real-IP $remote_addr;
# Provide either X-Original-URL and X-Forwarded-Proto or
# X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Uri or both.
# X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-URI or both.
# Those headers will be used by Authelia to deduce the target url of the user.
#
# X-Forwarded-Proto is mandatory since Authelia uses the "trust proxy" option.
@ -188,7 +188,7 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
proxy_set_header X-Forwarded-URI $request_uri;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -227,7 +227,7 @@ http {
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://smtp:1080;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -247,7 +247,7 @@ http {
resolver 127.0.0.11 ipv6=off;
set $upstream_endpoint http://duo-api:3000;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
@ -264,7 +264,7 @@ http {
listen 8080 ssl;
server_name _;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate /etc/ssl/server.cert;
ssl_certificate_key /etc/ssl/server.key;
return 301 https://home.example.com:8080/;

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-----
MIICXQIBAAKBgQDNloThcTVDIU1uscEdGFIDndqcCwnmYF4bs6bpJ1BxkBhqGUNY
Ru12hjiHLSA80ZwhNxZ4T5YD4+81Gvs9zMoSGd4jJRSBX6evPTXR8zkmcQI/EtN7
WgXOXhTx3JiIGlPOI3OGJR+rvfc9FiNx30FC1wpw3gt2ZC+NQeedDvdPKwIDAQAB
AoGBAIwGcfkO30UawJ+daDeF4g5ejI/toM+NYWuiwBNbWJoQl+Bj1o+gt4obvxKq
tKNX7OxelepZ4oZB0CIuf2LHQfU6cVGdu//or7nfS2FLBYStopZyL6KorZbkqsj1
ikQN4GosJQqaYkexnwjItMFaHaRRX6YnIXp42Jl1glitO3+5AkEA+thn/vwFo24I
fC+7ORpmLi+BVAkTuhMm+C6TIV6s64B+A5oQ82OBCYK9YCOWmS6JHHFDrxJla+3M
2U9KXky63wJBANHQCFCirfuT6esSjbqpCeqtmZG5LWHtL12V9DF7yjHPjmHL9uRu
e9W+Uz33IJbqd82gtZ/ARfpYEjD0JEieQTUCQFo872xzDTQ1qSfDo/5u2MNUo5mv
ikEuEp7FYnhmrp4poyt4iRCFgy4Ask+bfdmtO/XXaRnZ7FJfQYoLVB2ITNECQQCN
gOiauZztl4yj5heAVJFDnWF9To61BOp1C7VtyjdL8NfuTUluNrV+KqapnAp2vhue
q0zTOTH47X0XVxFBiLohAkBuQzPey5I3Ui8inE4sDt/fqX8r/GMhBTxIb9KlV/H6
jKZNs/83n5/ohaX36er8svW9PB4pcqENZ+kBpvDtKVwS
MIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S
KcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X
yEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M
lqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE
lgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R
cMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn
V3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv
B7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd
zV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036
UxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1
/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI
F4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd
7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs
hcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA
06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh
IlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75
HmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/
rW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE
Zrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b
bx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq
0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS
qfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2
qSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L
zqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2
HEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==
-----END RSA PRIVATE KEY-----

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

View File

@ -2,7 +2,7 @@
start_apps() {
# Create TLS certificate and key for HTTPS termination
kubectl create secret generic test-app-tls --namespace=authelia --from-file=apps/ssl/tls.key --from-file=apps/ssl/tls.crt
kubectl create secret generic test-app-tls --namespace=authelia --from-file=apps/ssl/server.key --from-file=apps/ssl/server.cert
# Spawn the applications
kubectl apply -f apps
@ -13,7 +13,11 @@ start_ingress_controller() {
}
start_dashboard() {
kubectl apply -f dashboard
kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta4/aio/deploy/recommended.yaml
kubectl apply -f dashboard.yml
echo "Bearer token for UI user."
kubectl -n kubernetes-dashboard describe secret $(kubectl -n kubernetes-dashboard get secret | grep admin-user | awk '{print $1}')
}
# Spawn Redis and Mongo as backend for Authelia
@ -28,6 +32,7 @@ start_mail() {
}
start_ldap() {
kubectl create configmap ldap-config --namespace=authelia --from-file=ldap/base.ldif --from-file=ldap/access.rules
kubectl apply -f ldap
}

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
ports:
- containerPort: 389
env:
- name: SLAPD_ORGANISATION
value: MyCompany
- name: SLAPD_DOMAIN
value: example.com
- name: SLAPD_PASSWORD
value: password
- name: SLAPD_CONFIG_PASSWORD
value: password
- name: SLAPD_ADDITIONAL_MODULES
value: memberof
- name: SLAPD_ADDITIONAL_SCHEMAS
value: openldap
- name: SLAPD_FORCE_RECONFIGURE
value: "true"
volumeMounts:
- name: config-volume
mountPath: /etc/ldap.dist/prepopulate
volumes:
- name: config-volume
configMap:
name: ldap-config
items:
- key: base.ldif
path: base.ldif
- key: access.rules
path: access.rules

36
handlers/const.go 100644
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 100644
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