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 26e57b06cdbfd0c913ae632bdc6381ef3f2b25c2
parent 30db0bc62f4c1f9f6ae93b82f4bb62074abe86dd
Author: Sebastian Mancke <s.mancke@tarent.de>
Date:   Mon, 14 Nov 2016 22:20:11 +0100

initial publishing of loginsrv

Diffstat:
MREADME.md | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Adev_run.sh | 3+++
Alogin/backend.go | 9+++++++++
Alogin/config.go | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alogin/config_test.go | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alogin/handler.go | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alogin/handler_test.go | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alogin/login_form.go | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alogin/provider.go | 27+++++++++++++++++++++++++++
Alogin/provider_description.go | 16++++++++++++++++
Alogin/simple_backend.go | 40++++++++++++++++++++++++++++++++++++++++
Alogin/simple_backend_test.go | 46++++++++++++++++++++++++++++++++++++++++++++++
Alogin/user_info.go | 11+++++++++++
Amain.go | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Aosiam/osiam_backend.go | 37+++++++++++++++++++++++++++++++++++++
Aosiam/setup.go | 19+++++++++++++++++++
16 files changed, 831 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md @@ -1,2 +1,73 @@ # loginsrv -Login server providing a JWT login for multiple login backends + +loginsrv is a standalone minimalistic login server providing a (JWT)[https://jwt.io/] login for multiple login backends. + +## Abstract + +Loginsrv provides a minimal endpoint for authentication. The login is +then performed against the providers and returned as Json Web Token. + +## Supported Provider +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. + +## Future Planed Features +- Support for 3-leged-Oauth2 flow (OSIAM, Google, Facebook login) +- Backend for checking agains .htaccess file +- Caddyserver middleware + +## API + +### GET /login + +Returns a simple bootstrap styled login form. + +The returned html follows the ui composition conventions from (lib-compose)[https://github.com/tarent/lib-compose], +so it can be embedded into an existing layout. + +### POST /login + +Does the login and returns the JWT. Depending on the content-type, and parameters a classical JSON-Rest or a redirect can be performed. + +#### Parameters + +| Parameter-Type | Parameter | Description | | +| ------------------|--------------------------------------------------|-----------------------------------------------------------|----------| +| Http-Header | Accept: text/html | Set the JWT-Token as Cookie 'jwt_token'. | default | +| Http-Header | Accept: application/jwt | Returns the JWT-Token within the body. No Cookie is set. | | +| Http-Header | Content-Type: application/x-www-form-urlencoded | Expect the credentials as form encoded parameters. | default | +| Http-Header | Content-Type: application/json | Take the credentials from the provided json object. | | +| Post-Parameter | username | The username | | +| Post-Parameter | password | The password | | +| Config-Parameter | success-url | The url to redirect on success | (default /) | + +#### Possible Return Codes + +| Code | Meaning | Description | +|------| ----------------------|----------------------------| +| 200 | OK | Successfully authenticated | +| 403 | Forbidden | The Credentials are wrong | +| 400 | Bad Request | Missing parameters | +| 500 | Internal Server Error | Internal error, e.g. the login provider is not available or failed | +| 303 | See Other | Sets the JWT as a cookie, if the login succeeds and redirect to the urls provided in `redirectSuccess` or `redirectError` | + +Hint: The status `401 Unauthorized` is not used as a return code to not conflict with an Http BasicAuth Authentication. + +#### Example for classical REST call +``` +curl -I -X POST -H "Accept: application/jwt" -H "Content-Type: application/json" --data '{"username": "bob", "password": "secret"}' http://example.com/login +HTTP/1.1 200 OK + +xxxxx.yyyyy.zzzzz +``` + +#### Example for form based web flow +``` +curl -I -X POST --data "username=bob&password=secret" http://example.com/login +HTTP/1.1 303 Moved Temporary +Set-Cookie: jwt_token=xxxxx.yyyyy.zzzzz +Location: /startpage +``` diff --git a/dev_run.sh b/dev_run.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +go run main.go --text-logging=true --jwt-secret=secret --backend "provider=simple,bob=secret" diff --git a/login/backend.go b/login/backend.go @@ -0,0 +1,9 @@ +package login + +type Backend interface { + // Authenticate checks the username/password against the backend. + // On success it returns true ans a UserInfo object which has at least the username set. + // If the credentials do not match, false is returned. + // The error parameter is nil, unless a communication error with the backend occured. + Authenticate(username, password string) (bool, UserInfo, error) +} diff --git a/login/config.go b/login/config.go @@ -0,0 +1,97 @@ +package login + +import ( + "flag" + "fmt" + "github.com/caarlos0/env" + "math/rand" + "strings" + "time" +) + +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +func init() { + rand.Seed(time.Now().UTC().UnixNano()) +} + +type BackendOptions []map[string]string + +func (bo *BackendOptions) String() string { + return fmt.Sprintf("%v", *bo) +} + +func (bo *BackendOptions) Set(value string) error { + optionMap, err := parseBackendOptions(value) + if err != nil { + return err + } + *bo = append(*bo, optionMap) + return nil +} + +type Config struct { + Host string `env:"LOGINSRV_HOST"` + Port string `env:"LOGINSRV_PORT"` + LogLevel string `env:"LOGINSRV_LOG_LEVEL"` + TextLogging bool `env:"LOGINSRV_TEXT_LOGGING"` + JwtSecret string `env:"LOGINSRV_JWT_SECRET"` + SuccessUrl string `env:"LOGINSRV_SUCCESS_URL"` + CookieName string `env:"LOGINSRV_COOKIE_NAME"` + CookieHttpOnly bool `env:"LOGINSRV_COOKIE_HTTP_ONLY"` + Backends BackendOptions +} + +func DefaultConfig() *Config { + return &Config{ + Host: "localhost", + Port: "6789", + LogLevel: "info", + JwtSecret: randStringBytes(32), + SuccessUrl: "/", + CookieName: "jwt_token", + CookieHttpOnly: true, + Backends: BackendOptions{}, + } +} +func ReadConfig() *Config { + config := DefaultConfig() + + env.Parse(config) + + flag.StringVar(&config.Host, "host", config.Host, "The host to listen on") + flag.StringVar(&config.Port, "port", config.Port, "The port to listen on") + flag.StringVar(&config.LogLevel, "log-level", config.LogLevel, "The log level") + flag.BoolVar(&config.TextLogging, "text-logging", config.TextLogging, "Log in text format instead of json") + flag.StringVar(&config.JwtSecret, "jwt-secret", "random key", "The secret to sign the jwt token") + flag.StringVar(&config.CookieName, "cookie-name", config.CookieName, "The name of the jwt cookie") + flag.BoolVar(&config.CookieHttpOnly, "cookie-http-only", config.CookieHttpOnly, "Set the cookie with the http only flag") + flag.StringVar(&config.SuccessUrl, "success-url", config.SuccessUrl, "The url to redirect after login") + flag.Var(&config.Backends, "backend", "Backend configuration in form 'provider=name,key=val,key=...', can be declared multiple times") + + flag.Parse() + return config +} + +func parseBackendOptions(b string) (map[string]string, error) { + opts := map[string]string{} + pairs := strings.Split(b, ",") + for _, p := range pairs { + pair := strings.SplitN(p, "=", 2) + if len(pair) != 2 { + return nil, fmt.Errorf("provider configuration has to be in form 'provider=name,key1=value1,key2=..', but was %v", p) + } + opts[pair[0]] = pair[1] + } + if _, exist := opts["provider"]; !exist { + return nil, fmt.Errorf("no provider name specified in %v", b) + } + return opts, nil +} +func randStringBytes(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +} diff --git a/login/config_test.go b/login/config_test.go @@ -0,0 +1,60 @@ +package login + +import ( +//"fmt" +//"github.com/stretchr/testify/assert" +//"testing" +) + +/** +func TestConfig_GetBackendOptions(t *testing.T) { + testCases := []struct { + backends []string + parsedOptions BackendOptions + expectError bool + }{ + { + []string{}, + []map[string]string{}, + false, + }, + { + []string{ + "name=p1,key1=value1,key2=value2", + "name=p2,key3=value3,key4=value4", + }, + []map[string]string{ + map[string]string{ + "name": "p1", + "key1": "value1", + "key2": "value2", + }, + map[string]string{ + "name": "p2", + "key3": "value3", + "key4": "value4", + }, + }, + false, + }, + { + []string{"foo"}, + nil, + true, + }, + } + for i, test := range testCases { + t.Run(fmt.Sprintf("test %v", i), func(t *testing.T) { + cfg := &Config{} + cfg.Backends = test.backends + opts, err := cfg.GetBackendOptions() + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, opts, test.parsedOptions) + } + }) + } +} +**/ diff --git a/login/handler.go b/login/handler.go @@ -0,0 +1,170 @@ +package login + +import ( + "errors" + "fmt" + "github.com/dgrijalva/jwt-go" + "github.com/tarent/lib-compose/logging" + "net/http" + "strings" +) + +type Handler struct { + backends []Backend + config *Config +} + +// NewHandler creates a login handler based on the supplied configuration. +func NewHandler(config *Config) (*Handler, error) { + backends := []Backend{} + for _, opt := range config.Backends { + p, exist := GetProvider(opt["provider"]) + if !exist { + return nil, fmt.Errorf("No such provider: %v", opt["provider"]) + } + b, err := p(opt) + if err != nil { + return nil, err + } + backends = append(backends, b) + } + if len(backends) == 0 { + return nil, errors.New("No login backends configured!") + } + return &Handler{ + backends: backends, + config: config, + }, nil +} + +func (h *Handler) authenticate(username, password string) (bool, UserInfo, error) { + for _, b := range h.backends { + authenticated, userInfo, err := b.Authenticate(username, password) + if err != nil { + return false, UserInfo{}, err + } + if authenticated { + return authenticated, userInfo, nil + } + } + return false, UserInfo{}, nil +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if !strings.HasSuffix(r.URL.Path, "/login") { + w.WriteHeader(404) + fmt.Fprintf(w, "404 Ressource not found") + return + } + + contentType := r.Header.Get("Content-Type") + if !(r.Method == "GET" || + (r.Method == "POST" && + (strings.HasPrefix(contentType, "application/json") || + strings.HasPrefix(contentType, "application/x-www-form-urlencoded") || + strings.HasPrefix(contentType, "multipart/form-data")))) { + w.WriteHeader(400) + fmt.Fprintf(w, "Bad Request: Method or content-type not supported") + return + } + + r.ParseForm() + if r.Method == "GET" { + writeLoginForm(w, + map[string]interface{}{ + "path": r.URL.Path, + "config": h.config, + }) + return + } + + if r.Method == "POST" { + username, password := getCredentials(r) + authenticated, userInfo, err := h.authenticate(username, password) + if err != nil { + logging.Application(r.Header).WithError(err).Error() + h.respondError(w, r) + return + } + + if authenticated { + logging.Application(r.Header). + WithField("username", username).Info("sucessfully authenticated") + h.respondAuthenticated(w, r, userInfo) + return + } + logging.Application(r.Header). + WithField("username", username).Info("failed authentication") + + h.respondAuthFailure(w, r) + return + } +} + +func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, userInfo UserInfo) { + token, err := h.createToken(userInfo) + if err != nil { + logging.Application(r.Header).WithError(err).Error() + h.respondError(w, r) + return + } + if wantHtml(r) { + + // TODO: set livetime + cookie := &http.Cookie{Name: h.config.CookieName, Value: token, HttpOnly: true} + http.SetCookie(w, cookie) + w.Header().Set("Location", h.config.SuccessUrl) + w.WriteHeader(303) + return + } + + w.Header().Set("Content-Type", "application/jwt") + w.WriteHeader(200) + fmt.Fprintf(w, "%s\n", token) +} + +func (h *Handler) createToken(userInfo UserInfo) (string, error) { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, userInfo) + return token.SignedString([]byte(h.config.JwtSecret)) +} + +func (h *Handler) respondError(w http.ResponseWriter, r *http.Request) { + if wantHtml(r) { + username, _ := getCredentials(r) + writeLoginForm(w, + map[string]interface{}{ + "path": r.URL.Path, + "error": true, + "config": h.config, + "username": username, + }) + return + } + w.WriteHeader(500) + fmt.Fprintf(w, "Internal Server Error") +} + +func (h *Handler) respondAuthFailure(w http.ResponseWriter, r *http.Request) { + if wantHtml(r) { + username, _ := getCredentials(r) + writeLoginForm(w, + map[string]interface{}{ + "path": r.URL.Path, + "failure": true, + "config": h.config, + + "username": username, + }) + return + } + w.WriteHeader(403) + fmt.Fprintf(w, "Wrong credentials") +} + +func wantHtml(r *http.Request) bool { + return strings.Contains(r.Header.Get("Accept"), "text/html") +} + +func getCredentials(r *http.Request) (string, string) { + return r.PostForm.Get("username"), r.PostForm.Get("password") +} diff --git a/login/handler_test.go b/login/handler_test.go @@ -0,0 +1,119 @@ +package login + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +const TypeJson = "Content-Type: application/json" +const TypeForm = "Content-Type: application/x-www-form-urlencoded" +const AcceptHtml = "Accept: text/html" +const AcceptJwt = "Accept: application/jwt" + +func TestHandler_NewFromConfig(t *testing.T) { + + testCases := []struct { + config *Config + backendCount int + expectError bool + }{ + { + &Config{Backends: BackendOptions{map[string]string{"provider": "simple", "bob": "secret"}}}, + 1, + false, + }, + // error cases + { + &Config{}, + 0, + true, + }, + { + &Config{Backends: BackendOptions{map[string]string{"foo": ""}}}, + 1, + true, + }, + { + &Config{Backends: BackendOptions{map[string]string{"provider": "simpleFoo", "bob": "secret"}}}, + 1, + true, + }, + } + for i, test := range testCases { + t.Run(fmt.Sprintf("test %v", i), func(t *testing.T) { + h, err := NewHandler(test.config) + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, 1, len(h.backends)) + } + }) + } +} + +func TestHandler_404(t *testing.T) { + recorder := call(req("GET", "/foo", "")) + assert.Equal(t, recorder.Code, 404) +} + +func TestHandler_LoginForm(t *testing.T) { + recorder := call(req("GET", "/context/login", "")) + assert.Equal(t, recorder.Code, 200) + assert.Contains(t, recorder.Body.String(), "form") + assert.Contains(t, recorder.Body.String(), `method="POST"`) + assert.Contains(t, recorder.Body.String(), `action="/context/login"`) +} + +func TestHandler_HEAD(t *testing.T) { + recorder := call(req("HEAD", "/context/login", "")) + assert.Equal(t, recorder.Code, 400) +} + +func TestHandler_LoginWeb(t *testing.T) { + // redirectSuccess + recorder := call(req("POST", "/context/login", "username=bob&password=secret", TypeForm, AcceptHtml)) + assert.Equal(t, 303, recorder.Code) + assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=") + assert.Equal(t, "/", recorder.Header().Get("Location")) + + // show the login after error + recorder = call(req("POST", "/context/login", "username=bob&password=FOOBAR", TypeForm, AcceptHtml)) + assert.Equal(t, 200, recorder.Code) + assert.Contains(t, recorder.Body.String(), "form") + assert.Contains(t, recorder.Body.String(), `method="POST"`) + assert.Contains(t, recorder.Body.String(), `action="/context/login"`) + assert.Equal(t, recorder.Header().Get("Set-Cookie"), "") +} + +func testHandler() *Handler { + return &Handler{ + backends: []Backend{ + NewSimpleBackend(map[string]string{"bob": "secret"}), + }, + config: DefaultConfig(), + } +} + +func call(req *http.Request) *httptest.ResponseRecorder { + recorder := httptest.NewRecorder() + h := testHandler() + h.ServeHTTP(recorder, req) + return recorder +} + +func req(method string, url string, body string, header ...string) *http.Request { + r, err := http.NewRequest(method, url, strings.NewReader(body)) + if err != nil { + panic(err) + } + for _, h := range header { + pair := strings.SplitN(h, ": ", 2) + r.Header.Add(pair[0], pair[1]) + } + return r +} diff --git a/login/login_form.go b/login/login_form.go @@ -0,0 +1,54 @@ +package login + +import ( + "html/template" + "io" +) + +const loginForm = `<!DOCTYPE html> +<html> + <head> + <link uic-remove rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> + <style> + .vertical-offset-100{ + padding-top:100px; + } + </style> + </head> + <body> + <uic-fragment name="content"> +<div class="container"> + <div class="row vertical-offset-100"> + <div class="col-md-4 col-md-offset-4"> + <div class="panel panel-default"> + <div class="panel-heading"> + <h3 class="panel-title">Please sign in</h3> + {{ if .error}}Internal error. Please try again later{{end}} + {{ if .failure}}Wrong credentials{{end}} + </div> + <div class="panel-body"> + <form accept-charset="UTF-8" role="form" method="POST" action="{{.path}}"> + <fieldset> + <div class="form-group"> + <input class="form-control" placeholder="Username" name="username" value="{{.username}}" type="text"> + </div> + <div class="form-group"> + <input class="form-control" placeholder="Password" name="password" type="password" value=""> + </div> + <input class="btn btn-lg btn-success btn-block" type="submit" value="Login"> + </fieldset> + </form> + </div> + </div> + </div> + </div> +</div> + </uic-fragment> + </body> +</html> +` + +func writeLoginForm(w io.Writer, params map[string]interface{}) { + t := template.Must(template.New("loginForm").Parse(loginForm)) + t.Execute(w, params) +} diff --git a/login/provider.go b/login/provider.go @@ -0,0 +1,27 @@ +package login + +// Provider is a factory method for creation of login backends. +type Provider func(config map[string]string) (Backend, error) + +var provider = map[string]Provider{} +var providerDescription = map[string]*ProviderDescription{} + +// RegisterProvider registers a factory method by the provider name. +func RegisterProvider(desc *ProviderDescription, factoryMethod Provider) { + provider[desc.Name] = factoryMethod + providerDescription[desc.Name] = desc +} + +// GetProvider returns a registered provider by its name. +// The bool return parameter indicated, if there was such a provider. +func GetProvider(providerName string) (Provider, bool) { + p, exist := provider[providerName] + return p, exist +} + +// GetProviderDescription returns the description of a registered provider by its name. +// The bool return parameter indicated, if there was such a provider. +func GetProviderDescription(providerName string) (*ProviderDescription, bool) { + pd, exist := providerDescription[providerName] + return pd, exist +} diff --git a/login/provider_description.go b/login/provider_description.go @@ -0,0 +1,16 @@ +package login + +type ProviderDescription struct { + // the name of the provider + Name string + + // the config options, which the provider supports + options []ProviderOption +} + +type ProviderOption struct { + Name string + Description string + Default string + Required string +} diff --git a/login/simple_backend.go b/login/simple_backend.go @@ -0,0 +1,40 @@ +package login + +const SimpleProviderName = "simple" + +func init() { + RegisterProvider( + &ProviderDescription{ + Name: SimpleProviderName, + }, + SimpleBackendFactory) +} + +func SimpleBackendFactory(config map[string]string) (Backend, error) { + userPassword := map[string]string{} + for k, v := range config { + if k != "provider" && k != "name" { + userPassword[k] = v + } + } + return NewSimpleBackend(userPassword), nil +} + +// Simple backend, working on a map of username password pairs +type SimpleBackend struct { + userPassword map[string]string +} + +// NewSimpleBackend creates a new SIMPLE Backend and verifies the parameters. +func NewSimpleBackend(userPassword map[string]string) *SimpleBackend { + return &SimpleBackend{ + userPassword: userPassword, + } +} + +func (sb *SimpleBackend) Authenticate(username, password string) (bool, UserInfo, error) { + if p, exist := sb.userPassword[username]; exist && p == password { + return true, UserInfo{Username: username}, nil + } + return false, UserInfo{}, nil +} diff --git a/login/simple_backend_test.go b/login/simple_backend_test.go @@ -0,0 +1,46 @@ +package login + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSetup(t *testing.T) { + p, exist := GetProvider(SimpleProviderName) + assert.True(t, exist) + assert.NotNil(t, p) + + backend, err := p(map[string]string{ + "provider": "simple", + "name": "myFooProvider", + "bob": "secret", + }) + + assert.NoError(t, err) + assert.Equal(t, + map[string]string{ + "bob": "secret", + }, + backend.(*SimpleBackend).userPassword) +} + +func TestSimpleBackend_Authenticate(t *testing.T) { + backend := NewSimpleBackend(map[string]string{ + "bob": "secret", + }) + + authenticated, userInfo, err := backend.Authenticate("bob", "secret") + assert.True(t, authenticated) + assert.Equal(t, "bob", userInfo.Username) + assert.NoError(t, err) + + authenticated, userInfo, err = backend.Authenticate("bob", "fooo") + assert.False(t, authenticated) + assert.Equal(t, "", userInfo.Username) + assert.NoError(t, err) + + authenticated, userInfo, err = backend.Authenticate("", "") + assert.False(t, authenticated) + assert.Equal(t, "", userInfo.Username) + assert.NoError(t, err) +} diff --git a/login/user_info.go b/login/user_info.go @@ -0,0 +1,11 @@ +package login + +type UserInfo struct { + Username string `json:"sub"` +} + +// this interface implementation +// lets us use the user info as Claim for jwt-go +func (u UserInfo) Valid() error { + return nil +} diff --git a/main.go b/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "github.com/tarent/loginsrv/login" + _ "github.com/tarent/loginsrv/osiam" + + "github.com/tarent/lib-compose/logging" + "net/http" + "os" + "os/signal" + "syscall" +) + +const applicationName = "loginsrv" + +func main() { + config := login.ReadConfig() + if err := logging.Set(config.LogLevel, config.TextLogging); err != nil { + exit(nil, err) + } + + logShutdownEvent() + + logging.LifecycleStart(applicationName, config) + + h, err := login.NewHandler(config) + if err != nil { + exit(nil, err) + } + + handlerChain := logging.NewLogMiddleware(h) + + exit(nil, http.ListenAndServe(config.Host+":"+config.Port, handlerChain)) +} + +func logShutdownEvent() { + go func() { + c := make(chan os.Signal) + signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) + exit(<-c, nil) + }() +} + +func exit(signal os.Signal, err error) { + logging.LifecycleStop(applicationName, signal, err) + if err == nil { + os.Exit(0) + } else { + os.Exit(1) + } +} diff --git a/osiam/osiam_backend.go b/osiam/osiam_backend.go @@ -0,0 +1,37 @@ +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/setup.go b/osiam/setup.go @@ -0,0 +1,19 @@ +package osiam + +import ( + "github.com/tarent/loginsrv/login" +) + +const OsiamProviderName = "osiam" + +func init() { + login.RegisterProvider( + &login.ProviderDescription{ + Name: OsiamProviderName, + }, + OsiamBackendFactory) +} + +func OsiamBackendFactory(config map[string]string) (login.Backend, error) { + return NewOsiamBackend(config["endpoint"], config["clientId"], config["clientSecret"]) +}