Skip to content

Commit

Permalink
go-aah/aah#37 added basic auth and improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jeevatkm committed Jul 5, 2017
1 parent 6710e21 commit e4fd16a
Show file tree
Hide file tree
Showing 9 changed files with 466 additions and 12 deletions.
10 changes: 7 additions & 3 deletions scheme/base.go
Expand Up @@ -8,15 +8,19 @@ import (
"aahframework.org/ahttp.v0"
"aahframework.org/config.v0"
"aahframework.org/log.v0"
"aahframework.org/security.v0-unstable/acrypto"
"aahframework.org/security.v0-unstable/authc"
"aahframework.org/security.v0-unstable/authz"
)

// BaseAuth struct hold base implementation of aah framework's authentication schemes.
type BaseAuth struct {
authenticator authc.Authenticator
authorizer authz.Authorizer
appCfg *config.Config
authenticator authc.Authenticator
authorizer authz.Authorizer
appCfg *config.Config
scheme string
keyPrefix string
passwordEncoder acrypto.PasswordEncoder
}

// Init method typically implemented by extending struct.
Expand Down
148 changes: 148 additions & 0 deletions scheme/basic_auth.go
@@ -0,0 +1,148 @@
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm)
// go-aah/security source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.

package scheme

import (
"fmt"

"aahframework.org/ahttp.v0"
"aahframework.org/config.v0"
"aahframework.org/essentials.v0"
"aahframework.org/log.v0"
"aahframework.org/security.v0-unstable/acrypto"
"aahframework.org/security.v0-unstable/authc"
"aahframework.org/security.v0-unstable/authz"
)

type (
// BasicAuth struct is aah framework's ready to use Basic Authentication scheme.
BasicAuth struct {
BaseAuth
RealmName string

isFileRealm bool
subjectMap map[string]*basicSubjectInfo
}

basicSubjectInfo struct {
AuthcInfo *authc.AuthenticationInfo
AuthzInfo *authz.AuthorizationInfo
}
)

// Init method initializes the Basic authentication scheme from `security.auth_schemes`.
func (b *BasicAuth) Init(cfg *config.Config, keyName string) error {
b.appCfg = cfg

b.keyPrefix = "security.auth_schemes." + keyName
b.scheme = b.appCfg.StringDefault(b.keyPrefix+".scheme", "basic")

b.RealmName = b.appCfg.StringDefault(b.keyPrefix+".realm_name", "Authentication Required")

fileRealmPath := b.appCfg.StringDefault(b.keyPrefix+".file_realm", "")
b.isFileRealm = !ess.IsStrEmpty(fileRealmPath)

// Basic auth configured to use file based user source
if b.isFileRealm {
fileRealmCfg, err := config.LoadFile(fileRealmPath)
if err != nil {
return err
}
b.subjectMap = make(map[string]*basicSubjectInfo)

for _, username := range fileRealmCfg.Keys() {
password := fileRealmCfg.StringDefault(username+".password", "")
if ess.IsStrEmpty(password) {
return fmt.Errorf("basicauth: '%v' key is required", username+".password")
}

authcInfo := authc.NewAuthenticationInfo()
authcInfo.Principals = append(authcInfo.Principals, &authc.Principal{Value: username, IsPrimary: true})
authcInfo.Credential = []byte(password)

authzInfo := authz.NewAuthorizationInfo()
if roles, found := fileRealmCfg.StringList(username + ".roles"); found {
authzInfo.AddRole(roles...)
}

if permissions, found := fileRealmCfg.StringList(username + ".permissions"); found {
authzInfo.AddPermissionString(permissions...)
}

b.subjectMap[username] = &basicSubjectInfo{AuthcInfo: authcInfo, AuthzInfo: authzInfo}
}
}

pencoder := b.appCfg.StringDefault(b.keyPrefix+".password_encoder.type", "bcrypt")
var err error
b.passwordEncoder, err = acrypto.CreatePasswordEncoder(pencoder)

return err
}

// Scheme method return authentication scheme name.
func (b *BasicAuth) Scheme() string {
return b.scheme
}

// DoAuthenticate method calls the registered `Authenticator` with authentication token.
func (b *BasicAuth) DoAuthenticate(authcToken *authc.AuthenticationToken) (*authc.AuthenticationInfo, error) {
log.Info(authcToken)

var authcInfo *authc.AuthenticationInfo
if b.isFileRealm {
if subject, found := b.subjectMap[authcToken.Identity]; found {
authcInfo = subject.AuthcInfo
}
} else {
if b.authenticator == nil {
log.Warn("BasicAuth: authenticator is nil")
return nil, authc.ErrAuthenticatorIsNil
}

authcInfo = b.authenticator.GetAuthenticationInfo(authcToken)
}

if authcInfo == nil {
log.Error("Subject not found")
return nil, authc.ErrAuthenticationFailed
}

log.Info(authcInfo)

// Compare passwords
isPasswordOk := b.passwordEncoder.Compare(authcInfo.Credential, []byte(authcToken.Credential))
if !isPasswordOk {
log.Error("Subject credentials do not match")
return nil, authc.ErrAuthenticationFailed
}

return authcInfo, nil
}

// DoAuthorizationInfo method calls registered `Authorizer` with authentication information.
func (b *BasicAuth) DoAuthorizationInfo(authcInfo *authc.AuthenticationInfo) *authz.AuthorizationInfo {
if b.isFileRealm {
return b.subjectMap[authcInfo.PrimaryPrincipal().Value].AuthzInfo
}

if b.authorizer == nil {
log.Warn("BasicAuth: authorizer is nil")
return authz.NewAuthorizationInfo()
}

return b.authorizer.GetAuthorizationInfo(authcInfo)
}

// ExtractAuthenticationToken method extracts the authentication token information
// from the HTTP request.
func (b *BasicAuth) ExtractAuthenticationToken(r *ahttp.Request) *authc.AuthenticationToken {
username, password, _ := r.Raw.BasicAuth()
return &authc.AuthenticationToken{
Scheme: b.scheme,
Identity: username,
Credential: password,
}
}
192 changes: 192 additions & 0 deletions scheme/basic_auth_test.go
@@ -0,0 +1,192 @@
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm)
// go-aah/security source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.

package scheme

import (
"net/http"
"os"
"path/filepath"
"testing"

ahttp "aahframework.org/ahttp.v0"
config "aahframework.org/config.v0"
"aahframework.org/security.v0-unstable/authc"
"aahframework.org/security.v0-unstable/authz"
"aahframework.org/test.v0/assert"
)

func TestSchemeBasicAuthFileRealm(t *testing.T) {
securityAuthConfigStr := `
security {
auth_schemes {
basic_auth {
# HTTP Basic Auth Scheme
scheme = "basic"
realm_name = "Authentication Required"
# supplied dynamicall for test
file_realm = "path/to/file"
}
}
}
`

// BasicAuth initialize and assertion
basicAuth := BasicAuth{}
cfg, _ := config.ParseString(securityAuthConfigStr)

err := basicAuth.Init(cfg, "basic_auth")
assert.NotNil(t, err)
assert.Equal(t, "configuration does not exists: path/to/file", err.Error())

fileRealmPathError := filepath.Join(getTestdataPath(), "basic_auth_file_realm_error.conf")
cfg.SetString("security.auth_schemes.basic_auth.file_realm", fileRealmPathError)
err = basicAuth.Init(cfg, "basic_auth")
assert.NotNil(t, err)
assert.Equal(t, "basicauth: 'test2.password' key is required", err.Error())

fileRealmPath := filepath.Join(getTestdataPath(), "basic_auth_file_realm.conf")
cfg.SetString("security.auth_schemes.basic_auth.file_realm", fileRealmPath)
err = basicAuth.Init(cfg, "basic_auth")

assert.Nil(t, err)
assert.NotNil(t, basicAuth)
assert.NotNil(t, basicAuth.appCfg)
assert.NotNil(t, basicAuth.passwordEncoder)
assert.Equal(t, "Authentication Required", basicAuth.RealmName)
assert.Equal(t, "basic", basicAuth.Scheme())

// user value
test1 := basicAuth.subjectMap["test1"]
assert.NotNil(t, test1)
assert.Equal(t, "test1", test1.AuthcInfo.PrimaryPrincipal().Value)
assert.True(t, test1.AuthzInfo.HasRole("admin"))

// user value
test2 := basicAuth.subjectMap["test2"]
assert.NotNil(t, test2)
assert.Equal(t, "test2", test2.AuthcInfo.PrimaryPrincipal().Value)
assert.False(t, test2.AuthzInfo.HasRole("admin"))

// Authenticate - Success
req, _ := http.NewRequest("GET", "http://localhost:8080/doc.html", nil)
req.SetBasicAuth("test1", "welcome123")
areq := ahttp.ParseRequest(req, &ahttp.Request{})
authcToken := basicAuth.ExtractAuthenticationToken(areq)
authcInfo, err := basicAuth.DoAuthenticate(authcToken)
assert.Nil(t, err)
assert.Equal(t, "test1", authcInfo.PrimaryPrincipal().Value)

// Authorization
authzInfo := basicAuth.DoAuthorizationInfo(authcInfo)
assert.NotNil(t, authzInfo)
assert.True(t, authzInfo.HasRole("manager"))
assert.True(t, authzInfo.IsPermitted("newsletter:read"))

// Authenticate - Failure
req.SetBasicAuth("test2", "welcome@123")
areq = ahttp.ParseRequest(req, &ahttp.Request{})
authcToken = basicAuth.ExtractAuthenticationToken(areq)
authcInfo, err = basicAuth.DoAuthenticate(authcToken)
assert.NotNil(t, err)
assert.Equal(t, "security: authentication failed", err.Error())
assert.Nil(t, authcInfo)
}

type testBasicAuthentication struct {
}

func (tba *testBasicAuthentication) Init(cfg *config.Config) error {
return nil
}

func (tba *testBasicAuthentication) GetAuthenticationInfo(authcToken *authc.AuthenticationToken) *authc.AuthenticationInfo {
if authcToken == nil {
return authc.NewAuthenticationInfo()
}

if authcToken.Identity == "test1" {
authcInfo := authc.NewAuthenticationInfo()
authcInfo.Principals = append(authcInfo.Principals, &authc.Principal{Realm: "database", Value: "test1", IsPrimary: true})
authcInfo.Credential = []byte("$2y$10$2A4GsJ6SmLAMvDe8XmTam.MSkKojdobBVJfIU7GiyoM.lWt.XV3H6") // welcome123
return authcInfo
}
return nil
}

func (tba *testBasicAuthentication) GetAuthorizationInfo(authcInfo *authc.AuthenticationInfo) *authz.AuthorizationInfo {
return authz.NewAuthorizationInfo()
}

func TestSchemeBasicAuthCustom(t *testing.T) {
securityAuthConfigStr := `
security {
auth_schemes {
basic_auth {
# HTTP Basic Auth Scheme
scheme = "basic"
realm_name = "Authentication Required"
}
}
}
`

// BasicAuth initialize and assertion
basicAuth := BasicAuth{}
cfg, _ := config.ParseString(securityAuthConfigStr)

err := basicAuth.Init(cfg, "basic_auth")
assert.Nil(t, err)
assert.NotNil(t, basicAuth)
assert.NotNil(t, basicAuth.appCfg)
assert.NotNil(t, basicAuth.passwordEncoder)
assert.Equal(t, "Authentication Required", basicAuth.RealmName)
assert.Equal(t, "basic", basicAuth.Scheme())
assert.Nil(t, basicAuth.authenticator)
assert.Nil(t, basicAuth.authorizer)

// Authenticate - Success
req, _ := http.NewRequest("GET", "http://localhost:8080/doc.html", nil)
req.SetBasicAuth("test1", "welcome123")
areq := ahttp.ParseRequest(req, &ahttp.Request{})
authcToken := basicAuth.ExtractAuthenticationToken(areq)
authcInfo, err := basicAuth.DoAuthenticate(authcToken)
assert.NotNil(t, err)
assert.Equal(t, "security: authenticator is nil", err.Error())
assert.Nil(t, authcInfo)

// Authorization
authzInfo := basicAuth.DoAuthorizationInfo(authcInfo)
assert.NotNil(t, authzInfo)
assert.False(t, authzInfo.HasRole("manager"))
assert.False(t, authzInfo.IsPermitted("newsletter:read"))

// Custom
tba := &testBasicAuthentication{}
assert.Nil(t, basicAuth.SetAuthenticator(tba))
assert.Nil(t, basicAuth.SetAuthorizer(tba))

authcInfo, err = basicAuth.DoAuthenticate(authcToken)
assert.Nil(t, err)
assert.Equal(t, "test1", authcInfo.PrimaryPrincipal().Value)

// Authorization
authzInfo = basicAuth.DoAuthorizationInfo(authcInfo)
assert.NotNil(t, authzInfo)
assert.False(t, authzInfo.HasRole("manager"))
assert.False(t, authzInfo.IsPermitted("newsletter:read"))

authcToken.Identity = "newuser"
authcInfo, err = basicAuth.DoAuthenticate(authcToken)
assert.NotNil(t, err)
assert.True(t, err == authc.ErrAuthenticationFailed)
}

func getTestdataPath() string {
wd, _ := os.Getwd()
return filepath.Join(wd, "testdata")
}

0 comments on commit e4fd16a

Please sign in to comment.