loginsrv

Unnamed repository; edit this file 'description' to name the repository.
git clone git@jamesshield.xyz:repos/loginsrv.git
Log | Files | Refs | README | LICENSE

commit 730d29537bc227232eb473b69c6f885988a6c58d
parent a271f57da6670cd8166d76c652b6610e14129de7
Author: Sebastian Mancke <s.mancke@tarent.de>
Date:   Fri, 18 Nov 2016 18:58:19 +0100

added osiam login backend

Diffstat:
MREADME.md | 14++++++++++++++
Mlogin/config_test.go | 8+++-----
Aosiam/backend.go | 41+++++++++++++++++++++++++++++++++++++++++
Aosiam/client.go | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aosiam/error.go | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dosiam/osiam_backend.go | 37-------------------------------------
Aosiam/token.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 241 insertions(+), 42 deletions(-)

diff --git a/README.md b/README.md @@ -13,6 +13,7 @@ The following providers (login backends) are supported. - (OSIAM)[http://osiam.org/] OSIAM is a secure identity management solution providing REST based services for authentication and authorization. It implements the multplie OAuth2 flows, as well as SCIM for managing the user data. +- Simple (user/password pairs by configuration) ## Future Planed Features - Support for 3-leged-Oauth2 flow (OSIAM, Google, Facebook login) @@ -88,3 +89,16 @@ HTTP/1.1 303 See Other Location: / Set-Cookie: jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib2IifQ.-51G5JQmpJleARHp8rIljBczPFanWT93d_N_7LQGUXU; HttpOnly ``` + + +## Provider + +### Osiam +To start loginsrv against the default osiam configuration on the same machine, use the following example. +``` +loginsrv --jwt-secret=jwtsecret --text-logging -backend 'provider=osiam,endpoint=http://localhost:8080,clientId=example-client,clientSecret=secret' +``` + +Then go to http://127.0.0.1:6789/login and login with `admin/koala`. + + diff --git a/login/config_test.go b/login/config_test.go @@ -1,12 +1,11 @@ package login import ( -//"fmt" -//"github.com/stretchr/testify/assert" -//"testing" + "fmt" + "github.com/stretchr/testify/assert" + "testing" ) -/** func TestConfig_GetBackendOptions(t *testing.T) { testCases := []struct { backends []string @@ -57,4 +56,3 @@ func TestConfig_GetBackendOptions(t *testing.T) { }) } } -**/ diff --git a/osiam/backend.go b/osiam/backend.go @@ -0,0 +1,41 @@ +package osiam + +import ( + "errors" + "fmt" + "github.com/tarent/loginsrv/login" + "net/url" +) + +type OsiamBackend struct { + client *Client +} + +// NewOsiamBackend creates a new OSIAM Backend and verifies the parameters. +func NewOsiamBackend(endpoint, clientId, clientSecret string) (*OsiamBackend, error) { + if _, err := url.Parse(endpoint); err != nil { + return nil, fmt.Errorf("osiam endpoint has to be a valid url: %v: %v", endpoint, err) + } + + if clientId == "" { + return nil, errors.New("No osiam clientId provided.") + } + if clientSecret == "" { + return nil, errors.New("No osiam clientSecret provided.") + } + client := NewClient(endpoint, clientId, clientSecret) + return &OsiamBackend{ + client: client, + }, nil +} + +func (b *OsiamBackend) Authenticate(username, password string) (bool, login.UserInfo, error) { + authenticated, _, err := b.client.GetTokenByPassword(username, password) + if !authenticated || err != nil { + return authenticated, login.UserInfo{}, err + } + userInfo := login.UserInfo{ + Username: username, + } + return true, userInfo, nil +} diff --git a/osiam/client.go b/osiam/client.go @@ -0,0 +1,81 @@ +package osiam + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +type Client struct { + Endpoint string + ClientId string + ClientSecret string +} + +func NewClient(endpoint string, clientId string, clientSecret string) *Client { + return &Client{ + Endpoint: endpoint, + ClientId: clientId, + ClientSecret: clientSecret, + } +} + +// Do an Osiam authorisation by Resource Owner Password Credentials Grant. +// If no scopes are supplied, the default scope ist 'me'. +func (c *Client) GetTokenByPassword(username, password string, scopes ...string) (authenticated bool, token *Token, err error) { + scopeList := strings.Join(scopes, ",") + if scopeList == "" { + scopeList = "ME" + } + + reqBody := fmt.Sprintf("grant_type=password&username=%v&password=%v&scope=%v", url.QueryEscape(username), url.QueryEscape(password), scopeList) + req, err := http.NewRequest("POST", c.Endpoint+"/oauth/token", strings.NewReader(reqBody)) + if err != nil { + return false, nil, err + } + + req.SetBasicAuth(c.ClientId, c.ClientSecret) + req.Header.Set("Content-type", "application/x-www-form-urlencoded") + + res, err := http.DefaultClient.Do(req) + if err != nil { + return false, nil, err + } + + if !isJson(res.Header.Get("Content-Type")) { + return false, nil, fmt.Errorf("Expected a token in json format, but got Content-Type: %v", res.Header.Get("Content-Type")) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return false, nil, err + } + + if res.StatusCode == 200 { + token = &Token{} + err = json.Unmarshal(body, token) + if err != nil { + return false, nil, err + } + return true, token, nil + } + + errorMessage := ParseOsiamError(body) + + if errorMessage.IsLoginError() { // wrong user credentials + return false, nil, nil + } + + if errorMessage.IsUnauthorized() { // wrong user credentials + return false, nil, fmt.Errorf("Osiam client credentials seem to be wrong, got message: %v, %v (http status %v)", errorMessage.Error, errorMessage.Message, res.StatusCode) + } + + return false, nil, fmt.Errorf("Osiam error: %v, %v (http status %v)", errorMessage.Error, errorMessage.Message, res.StatusCode) +} + +func isJson(contentType string) bool { + return strings.HasPrefix(contentType, "application/json") +} diff --git a/osiam/error.go b/osiam/error.go @@ -0,0 +1,55 @@ +package osiam + +import ( + "encoding/json" +) + +type OsiamError struct { + Error string + Message string +} + +func ParseOsiamError(jsonBody []byte) OsiamError { + m := map[string]interface{}{} + err := json.Unmarshal(jsonBody, &m) + if err != nil { + return OsiamError{ + "client_parse_error", + "osiam response is no valid json: " + string(jsonBody), + } + } + e := OsiamError{} + if v, exist := m["error"]; exist { + if vCasted, ok := v.(string); ok { + e.Error = vCasted + } + } + + if v, exist := m["message"]; exist { + if vCasted, ok := v.(string); ok { + e.Message = vCasted + } + } + + if v, exist := m["error_description"]; exist { + if vCasted, ok := v.(string); ok { + e.Message = vCasted + } + } + + if e.Error == "" && e.Message == "" { + return OsiamError{ + "client_parse_error", + "not a valid osiam error message: " + string(jsonBody), + } + } + return e +} + +func (e OsiamError) IsLoginError() bool { + return e.Error == "invalid_grant" +} + +func (e OsiamError) IsUnauthorized() bool { + return e.Error == "Unauthorized" +} diff --git a/osiam/osiam_backend.go b/osiam/osiam_backend.go @@ -1,37 +0,0 @@ -package osiam - -import ( - "errors" - "fmt" - "github.com/tarent/loginsrv/login" - "net/url" -) - -type OsiamBackend struct { - endpoint string - clientId string - clientSecret string -} - -// NewOsiamBackend creates a new OSIAM Backend and verifies the parameters. -func NewOsiamBackend(endpoint, clientId, clientSecret string) (*OsiamBackend, error) { - if _, err := url.Parse(endpoint); err != nil { - return nil, fmt.Errorf("osiam endpoint hast to be a valid url: %v: %v", endpoint, err) - } - - if clientId == "" { - return nil, errors.New("No osiam clientId provided.") - } - if clientSecret == "" { - return nil, errors.New("No osiam clientSecret provided.") - } - return &OsiamBackend{ - endpoint: endpoint, - clientId: clientId, - clientSecret: clientSecret, - }, nil -} - -func (ob *OsiamBackend) Authenticate(username, password string) (bool, login.UserInfo, error) { - return false, login.UserInfo{}, errors.New("Not implemented yet") -} diff --git a/osiam/token.go b/osiam/token.go @@ -0,0 +1,47 @@ +package osiam + +import ( + "fmt" + "strconv" + "time" +) + +// Token represents an osiam auth token +type Token struct { + TokenType string `json:"token_type"` // example "bearer" + AccessToken string `json:"access_token"` // example "79f479c2-c0d7-458a-8464-7eb887dbc943" + RefreshToken string `json:"refresh_token"` // example "3c7c4a87-dc91-4dd0-8ec8-d229a237a47c" + ClientId string `json:"client_id"` // example "example-client" + UserName string `json:"user_name"` // example "admin" + Userid string `json:"user_id"` // example "84f6cffa-4505-48ec-a851-424160892283" + Scope string `json:"scope"` // example "ME" + RefreshTokenExpiresAt Timestamp `json:"refresh_token_expires_at"` // example 1479309001813 + ExpiresAt Timestamp `json:"expires_at"` // example 1479251401814 + ExpiresIn int `json:"expires_in"` // example 28795 +} + +type Timestamp struct { + T time.Time +} + +func (timestamp *Timestamp) UnmarshalJSON(b []byte) (err error) { + i, err := strconv.ParseInt(string(b), 10, 64) + if err != nil { + return err + } + timestamp.T = time.Unix(i, 0) + return nil +} + +func (timestamp *Timestamp) MarshalJSON() ([]byte, error) { + if timestamp.T.UnixNano() == nilTime { + return []byte("null"), nil + } + return []byte(fmt.Sprintf("%d", timestamp.T.Unix())), nil +} + +var nilTime = (time.Time{}).UnixNano() + +func (timestamp *Timestamp) IsSet() bool { + return timestamp.T.UnixNano() != nilTime +}