Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
go-aah/aah#37 added basic auth and improvements
- Loading branch information
Showing
9 changed files
with
466 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} |
Oops, something went wrong.