JWT working
This commit is contained in:
parent
6aa2c46717
commit
824e54894e
5 changed files with 266 additions and 45 deletions
|
@ -20,6 +20,8 @@ var (
|
||||||
ErrDisabled = errors.New("user disabled")
|
ErrDisabled = errors.New("user disabled")
|
||||||
ErrInvalidAuth = errors.New("invalid auth")
|
ErrInvalidAuth = errors.New("invalid auth")
|
||||||
ErrInvalidHandler = errors.New("no such handler")
|
ErrInvalidHandler = errors.New("no such handler")
|
||||||
|
ErrInvalidIP = errors.New("invalid IP")
|
||||||
|
ErrUserAuthRemote = errors.New("user cannot authenticate remotely")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
|
|
|
@ -107,7 +107,7 @@ func (f *LoginFlow) progress(a *Authenticator, c echo.Context) error {
|
||||||
user, clientID, err := a.Check(f, c.Request(), rm)
|
user, clientID, err := a.Check(f, c.Request(), rm)
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
creds := a.store.Credential(user)
|
creds := a.store.GetCredential(user)
|
||||||
finishedFlow := flow.Result{}
|
finishedFlow := flow.Result{}
|
||||||
a.flows.Remove(f)
|
a.flows.Remove(f)
|
||||||
copier.Copy(&finishedFlow, f)
|
copier.Copy(&finishedFlow, f)
|
||||||
|
|
|
@ -2,42 +2,19 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"dynatron.me/x/blasphem/internal/common"
|
"dynatron.me/x/blasphem/internal/common"
|
||||||
"dynatron.me/x/blasphem/internal/generate"
|
"dynatron.me/x/blasphem/internal/generate"
|
||||||
"dynatron.me/x/blasphem/pkg/auth/provider"
|
"dynatron.me/x/blasphem/pkg/auth/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
|
||||||
TokenType string
|
|
||||||
RefreshTokenID string
|
|
||||||
)
|
|
||||||
|
|
||||||
type RefreshToken struct {
|
|
||||||
ID RefreshTokenID `json:"id"`
|
|
||||||
UserID UserID `json:"user_id"`
|
|
||||||
ClientID *ClientID `json:"client_id"`
|
|
||||||
ClientName *string `json:"client_name"`
|
|
||||||
ClientIcon *string `json:"client_icon"`
|
|
||||||
TokenType TokenType `json:"token_type"`
|
|
||||||
CreatedAt *common.PyTimestamp `json:"created_at"`
|
|
||||||
AccessTokenExpiration json.Number `json:"access_token_expiration"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
JWTKey string `json:"jwt_key"`
|
|
||||||
LastUsedAt *common.PyTimestamp `json:"last_used_at"`
|
|
||||||
LastUsedIP *string `json:"last_used_ip"`
|
|
||||||
CredentialID *CredID `json:"credential_id"`
|
|
||||||
Version *string `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rt *RefreshToken) IsValid() bool {
|
|
||||||
return rt.JWTKey != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
type authCodeStore struct {
|
type authCodeStore struct {
|
||||||
s map[authCodeTuple]flowResult
|
s map[authCodeTuple]flowResult
|
||||||
lastCull time.Time
|
lastCull time.Time
|
||||||
|
@ -99,7 +76,6 @@ func (ss *authCodeStore) verify(tr *TokenRequest, r *http.Request) (*Credentials
|
||||||
key := authCodeTuple{tr.ClientID, tr.Code}
|
key := authCodeTuple{tr.ClientID, tr.Code}
|
||||||
if t, hasCode := ss.s[key]; hasCode {
|
if t, hasCode := ss.s[key]; hasCode {
|
||||||
defer delete(ss.s, key)
|
defer delete(ss.s, key)
|
||||||
// TODO: JWT
|
|
||||||
if t.IsValid(time.Now()) {
|
if t.IsValid(time.Now()) {
|
||||||
return t.Cred, true
|
return t.Cred, true
|
||||||
}
|
}
|
||||||
|
@ -138,6 +114,156 @@ func (cred *Credentials) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(nCd)
|
return json.Marshal(nCd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
TokenType string
|
||||||
|
RefreshTokenID string
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TokenTypeSystem TokenType = "system"
|
||||||
|
TokenTypeNormal TokenType = "normal"
|
||||||
|
TokenTypeLongLived TokenType = "long_lived_access_token"
|
||||||
|
TokenTypeNone TokenType = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
func (tt TokenType) IsValid() bool {
|
||||||
|
switch tt {
|
||||||
|
case TokenTypeSystem, TokenTypeNormal, TokenTypeLongLived:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshToken struct {
|
||||||
|
ID RefreshTokenID `json:"id"`
|
||||||
|
UserID UserID `json:"user_id"`
|
||||||
|
ClientID *ClientID `json:"client_id"`
|
||||||
|
ClientName *string `json:"client_name"`
|
||||||
|
ClientIcon *string `json:"client_icon"`
|
||||||
|
TokenType TokenType `json:"token_type"`
|
||||||
|
CreatedAt *common.PyTimestamp `json:"created_at"`
|
||||||
|
AccessTokenExpiration json.Number `json:"access_token_expiration"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
JWTKey string `json:"jwt_key"`
|
||||||
|
LastUsedAt *common.PyTimestamp `json:"last_used_at"`
|
||||||
|
LastUsedIP *string `json:"last_used_ip"`
|
||||||
|
CredentialID *CredID `json:"credential_id"`
|
||||||
|
Version *string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rt *RefreshToken) IsValid() bool {
|
||||||
|
return rt.JWTKey != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type RefreshOption func(*RefreshToken)
|
||||||
|
|
||||||
|
func WithClientID(cid ClientID) RefreshOption {
|
||||||
|
return func(rt *RefreshToken) {
|
||||||
|
rt.ClientID = &cid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithClientName(n string) RefreshOption {
|
||||||
|
return func(rt *RefreshToken) {
|
||||||
|
rt.ClientName = &n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithClientIcon(n string) RefreshOption {
|
||||||
|
return func(rt *RefreshToken) {
|
||||||
|
rt.ClientIcon = &n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTokenType(t TokenType) RefreshOption {
|
||||||
|
return func(rt *RefreshToken) {
|
||||||
|
rt.TokenType = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCredential(c *Credentials) RefreshOption {
|
||||||
|
return func(rt *RefreshToken) {
|
||||||
|
rt.CredentialID = &c.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultAccessExpiration = "1800"
|
||||||
|
|
||||||
|
func (a *Authenticator) NewRefreshToken(user *User, opts ...RefreshOption) (*RefreshToken, error) {
|
||||||
|
e := func(es string, a ...interface{}) (*RefreshToken, error) {
|
||||||
|
return nil, fmt.Errorf(es, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
now := common.PyTimestamp(time.Now())
|
||||||
|
|
||||||
|
r := &RefreshToken{
|
||||||
|
ID: RefreshTokenID(generate.UUID()),
|
||||||
|
UserID: user.ID,
|
||||||
|
Token: generate.Hex(64),
|
||||||
|
JWTKey: generate.Hex(64),
|
||||||
|
CreatedAt: &now,
|
||||||
|
AccessTokenExpiration: DefaultAccessExpiration,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.TokenType == TokenTypeNone {
|
||||||
|
if user.SystemGenerated {
|
||||||
|
r.TokenType = TokenTypeSystem
|
||||||
|
} else {
|
||||||
|
r.TokenType = TokenTypeNormal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case !r.TokenType.IsValid():
|
||||||
|
return e("invalid token type")
|
||||||
|
case !user.Active:
|
||||||
|
return e("user is not active")
|
||||||
|
case user.SystemGenerated && r.ClientID != nil:
|
||||||
|
return e("system generated users cannot have refresh tokens connected to a client")
|
||||||
|
case !r.TokenType.IsValid():
|
||||||
|
return e("invalid token type '%v'", r.TokenType)
|
||||||
|
case user.SystemGenerated != (r.TokenType == TokenTypeSystem):
|
||||||
|
return e("system generated user can only have system type refresh tokens")
|
||||||
|
case r.TokenType == TokenTypeNormal && r.ClientID == nil:
|
||||||
|
return e("client is required to generate a refresh token")
|
||||||
|
case r.TokenType == TokenTypeLongLived && r.ClientName == nil:
|
||||||
|
return e("client name is required for long-lived token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.TokenType == TokenTypeLongLived {
|
||||||
|
for _, lv := range user.RefreshTokens {
|
||||||
|
if strPtrEq(lv.ClientName, r.ClientName) && lv.TokenType == TokenTypeLongLived {
|
||||||
|
return e("client name '%v' already exists", *r.ClientName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.store.PutRefreshToken(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RefreshToken) AccessToken(req *http.Request) (string, error) {
|
||||||
|
now := time.Now()
|
||||||
|
exp, err := r.AccessTokenExpiration.Int64()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pytnow := common.PyTimestamp(now)
|
||||||
|
r.LastUsedAt = &pytnow
|
||||||
|
r.LastUsedIP = &req.RemoteAddr
|
||||||
|
|
||||||
|
return jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"iss": r.ID,
|
||||||
|
"iat": now,
|
||||||
|
"exp": now.Add(time.Duration(exp) * time.Second),
|
||||||
|
}).SignedString([]byte(r.JWTKey))
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credentials {
|
func (a *Authenticator) verifyAndGetCredential(tr *TokenRequest, r *http.Request) *Credentials {
|
||||||
cred, success := a.authCodes.verify(tr, r)
|
cred, success := a.authCodes.verify(tr, r)
|
||||||
if !success {
|
if !success {
|
||||||
|
@ -179,6 +305,8 @@ type TokenRequest struct {
|
||||||
GrantType GrantType `form:"grant_type"`
|
GrantType GrantType `form:"grant_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const AuthFailed = "authentication failure"
|
||||||
|
|
||||||
func (a *Authenticator) TokenHandler(c echo.Context) error {
|
func (a *Authenticator) TokenHandler(c echo.Context) error {
|
||||||
rq := new(TokenRequest)
|
rq := new(TokenRequest)
|
||||||
err := c.Bind(rq)
|
err := c.Bind(rq)
|
||||||
|
@ -200,13 +328,45 @@ func (a *Authenticator) TokenHandler(c echo.Context) error {
|
||||||
// TODO: success
|
// TODO: success
|
||||||
user, err := a.getOrCreateUser(cred)
|
user, err := a.getOrCreateUser(cred)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.JSON(http.StatusUnauthorized, AuthError{Error: "access_denied", Description: err.Error()})
|
log.Error().Err(err).Msg("getOrCreateUser")
|
||||||
|
return c.JSON(http.StatusForbidden, AuthError{Error: "access_denied", Description: AuthFailed})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.allowedToAuth(); err != nil {
|
if err := user.allowedToAuth(c.Request()); err != nil {
|
||||||
return c.JSON(http.StatusUnauthorized, AuthError{Error: "access_denied", Description: err.Error()})
|
log.Error().Err(err).Msg("allowedToAuth")
|
||||||
|
return c.JSON(http.StatusForbidden, AuthError{Error: "access_denied", Description: AuthFailed})
|
||||||
}
|
}
|
||||||
return c.String(http.StatusOK, "token good I guess")
|
|
||||||
|
// TODO: create a refresh token, return it and refreshtoken.AccessToken()
|
||||||
|
rt, err := a.NewRefreshToken(user, WithClientID(rq.ClientID), WithCredential(cred))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("NewRefreshToken")
|
||||||
|
return c.JSON(http.StatusForbidden, AuthError{Error: "access_denied", Description: AuthFailed})
|
||||||
|
}
|
||||||
|
|
||||||
|
at, err := rt.AccessToken(c.Request())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("AccessToken")
|
||||||
|
return c.JSON(http.StatusForbidden, AuthError{Error: "access_denied", Description: AuthFailed})
|
||||||
|
}
|
||||||
|
|
||||||
|
exp, _ := rt.AccessTokenExpiration.Int64()
|
||||||
|
|
||||||
|
successResp := struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
HAAuthProvider string `json:"ha_auth_provider"`
|
||||||
|
}{
|
||||||
|
AccessToken: at,
|
||||||
|
TokenType: "Bearer",
|
||||||
|
RefreshToken: rt.Token,
|
||||||
|
ExpiresIn: exp,
|
||||||
|
HAAuthProvider: cred.AuthProviderType,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, &successResp)
|
||||||
}
|
}
|
||||||
case GTRefreshToken:
|
case GTRefreshToken:
|
||||||
return c.String(http.StatusNotImplemented, "not implemented")
|
return c.String(http.StatusNotImplemented, "not implemented")
|
||||||
|
|
|
@ -17,10 +17,13 @@ const (
|
||||||
|
|
||||||
type AuthStore interface {
|
type AuthStore interface {
|
||||||
User(UserID) *User
|
User(UserID) *User
|
||||||
Credential(provider.ProviderUser) *Credentials
|
GetCredential(provider.ProviderUser) *Credentials
|
||||||
|
PutRefreshToken(*RefreshToken) (*RefreshToken, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type authStore struct {
|
type authStore struct {
|
||||||
|
storage.Item `json:"-"`
|
||||||
|
|
||||||
Users []*User `json:"users"`
|
Users []*User `json:"users"`
|
||||||
Groups []*Group `json:"groups"`
|
Groups []*Group `json:"groups"`
|
||||||
Credentials []*Credentials `json:"credentials"`
|
Credentials []*Credentials `json:"credentials"`
|
||||||
|
@ -28,18 +31,27 @@ type authStore struct {
|
||||||
|
|
||||||
userMap map[UserID]*User
|
userMap map[UserID]*User
|
||||||
providerUsers map[provider.ProviderUser]*Credentials
|
providerUsers map[provider.ProviderUser]*Credentials
|
||||||
|
store storage.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (as *authStore) sync() {
|
||||||
|
err := as.store.Flush(as.ItemKey())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("sync authStore")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func strPtrEq(n1, n2 *string) bool {
|
func strPtrEq(n1, n2 *string) bool {
|
||||||
return (n1 == n2 || (n1 != nil && n2 != nil && *n1 == *n2))
|
return (n1 == n2 || (n1 != nil && n2 != nil && *n1 == *n2))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (as *authStore) Credential(p provider.ProviderUser) *Credentials {
|
func (as *authStore) GetCredential(p provider.ProviderUser) *Credentials {
|
||||||
for _, cr := range as.Credentials {
|
for _, cr := range as.Credentials {
|
||||||
if p.Provider() != nil &&
|
if p != nil && (p == cr.User ||
|
||||||
|
(p.Provider() != nil &&
|
||||||
strPtrEq(cr.AuthProviderID, p.Provider().ProviderID()) &&
|
strPtrEq(cr.AuthProviderID, p.Provider().ProviderID()) &&
|
||||||
cr.AuthProviderType == p.Provider().ProviderType() &&
|
cr.AuthProviderType == p.Provider().ProviderType() &&
|
||||||
p.Provider().EqualCreds(cr.User.UserData(), p.UserData()) {
|
p.Provider().EqualCreds(cr.User.UserData(), p.UserData()))) {
|
||||||
return cr
|
return cr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +59,23 @@ func (as *authStore) Credential(p provider.ProviderUser) *Credentials {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (as *authStore) PutRefreshToken(rt *RefreshToken) (*RefreshToken, error) {
|
||||||
|
e := func(es string, a ...interface{}) (*RefreshToken, error) {
|
||||||
|
return nil, fmt.Errorf(es, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, hasUser := as.userMap[rt.UserID]
|
||||||
|
if !hasUser {
|
||||||
|
return e("no such user %v", rt.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
as.Refresh = append(as.Refresh, rt)
|
||||||
|
u.RefreshTokens = append(u.RefreshTokens, rt)
|
||||||
|
|
||||||
|
as.sync()
|
||||||
|
return rt, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (as *authStore) newCredential(p provider.ProviderUser) *Credentials {
|
func (as *authStore) newCredential(p provider.ProviderUser) *Credentials {
|
||||||
// XXX: probably broken
|
// XXX: probably broken
|
||||||
prov := p.Provider()
|
prov := p.Provider()
|
||||||
|
@ -61,8 +90,14 @@ func (as *authStore) newCredential(p provider.ProviderUser) *Credentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) {
|
func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error) {
|
||||||
as = &authStore{}
|
as = &authStore{
|
||||||
err = s.Get(AuthStoreKey, as)
|
store: s,
|
||||||
|
}
|
||||||
|
|
||||||
|
as.Item, err = s.GetItem(AuthStoreKey, as)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
as.userMap = make(map[UserID]*User)
|
as.userMap = make(map[UserID]*User)
|
||||||
as.providerUsers = make(map[provider.ProviderUser]*Credentials)
|
as.providerUsers = make(map[provider.ProviderUser]*Credentials)
|
||||||
|
@ -94,7 +129,7 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
|
||||||
|
|
||||||
u, hasUser := as.userMap[c.UserID]
|
u, hasUser := as.userMap[c.UserID]
|
||||||
if !hasUser {
|
if !hasUser {
|
||||||
log.Error().Str("userid", string(c.UserID)).Msg("no such userid in map")
|
log.Error().Str("userid", string(c.UserID)).Msg("creds no such userid in map")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,8 +140,15 @@ func (a *Authenticator) newAuthStore(s storage.Store) (as *authStore, err error)
|
||||||
i := 0
|
i := 0
|
||||||
for _, rt := range as.Refresh {
|
for _, rt := range as.Refresh {
|
||||||
if rt.IsValid() {
|
if rt.IsValid() {
|
||||||
|
u, hasUser := as.userMap[rt.UserID]
|
||||||
|
if !hasUser {
|
||||||
|
log.Error().Str("userid", string(rt.UserID)).Msg("refreshtokens no such userid in map")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
as.Refresh[i] = rt
|
as.Refresh[i] = rt
|
||||||
i++
|
i++
|
||||||
|
u.RefreshTokens = append(u.RefreshTokens, rt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import ()
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
type UserID string
|
type UserID string
|
||||||
type GroupID string
|
type GroupID string
|
||||||
|
@ -18,6 +21,7 @@ type User struct {
|
||||||
UserMetadata
|
UserMetadata
|
||||||
|
|
||||||
Creds []*Credentials `json:"-"`
|
Creds []*Credentials `json:"-"`
|
||||||
|
RefreshTokens []*RefreshToken `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserMetadata struct {
|
type UserMetadata struct {
|
||||||
|
@ -28,14 +32,27 @@ type UserMetadata struct {
|
||||||
LocalOnly bool `json:"local_only"`
|
LocalOnly bool `json:"local_only"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) allowedToAuth() error {
|
func (u *User) allowedToAuth(r *http.Request) error {
|
||||||
if !u.Active {
|
if !u.Active {
|
||||||
return ErrDisabled
|
return ErrDisabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !u.LocalOnly {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(r.RemoteAddr)
|
||||||
|
if ip == nil {
|
||||||
|
return ErrInvalidIP
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrUserAuthRemote
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Authenticator) getOrCreateUser(c *Credentials) (*User, error) {
|
func (a *Authenticator) getOrCreateUser(c *Credentials) (*User, error) {
|
||||||
u := a.store.User(c.UserID)
|
u := a.store.User(c.UserID)
|
||||||
if u == nil {
|
if u == nil {
|
||||||
|
|
Loading…
Reference in a new issue