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