Skip to content

Commit

Permalink
Feature/ocisprovider use backend (#249)
Browse files Browse the repository at this point in the history
* TODOS

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>

* oidcprovider: allow auth and user backend config

* add todos and comments

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
  • Loading branch information
butonic authored and labkode committed Sep 19, 2019
1 parent 4556d4d commit ca23166
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 141 deletions.
37 changes: 24 additions & 13 deletions cmd/revad/svcs/httpsvcs/oidcprovider/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import (
"fmt"
"net/http"

typespb "github.com/cs3org/go-cs3apis/cs3/types"
"github.com/cs3org/reva/pkg/appctx"
"github.com/cs3org/reva/pkg/user"
)

func (s *svc) doAuth(w http.ResponseWriter, r *http.Request) {
Expand All @@ -31,10 +33,10 @@ func (s *svc) doAuth(w http.ResponseWriter, r *http.Request) {

// Let's create an AuthorizeRequest object!
// It will analyze the request and extract important information like scopes, response type and others.
ar, err := oauth2.NewAuthorizeRequest(ctx, r)
ar, err := s.oauth2.NewAuthorizeRequest(ctx, r)
if err != nil {
log.Error().Err(err).Msg("Error occurred in NewAuthorizeRequest")
oauth2.WriteAuthorizeError(w, ar, err)
s.oauth2.WriteAuthorizeError(w, ar, err)
return
}
// You have now access to authorizeRequest, Code ResponseTypes, Scopes ...
Expand All @@ -48,11 +50,11 @@ func (s *svc) doAuth(w http.ResponseWriter, r *http.Request) {
// We're simplifying things and just checking if the request includes a valid username and password
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("Error occurred parsing the form data")
oauth2.WriteAuthorizeError(w, ar, err)
s.oauth2.WriteAuthorizeError(w, ar, err)
return
}
username := r.PostForm.Get("username")
password := "secret"
password := r.PostForm.Get("password")
if username == "" {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, err := w.Write([]byte(fmt.Sprintf(`
Expand All @@ -63,19 +65,28 @@ func (s *svc) doAuth(w http.ResponseWriter, r *http.Request) {
By logging in, you consent to grant these scopes:
<ul>%s</ul>
</p>
<input type="text" name="username" /> <small>try aaliyah_abernathy, aaliyah_adams or aaliyah_anderson</small><br>
<input type="text" name="username" placeholder="Username" autofocus="autofocus"/><br>
<input type="password" name="password" placeholder="Password"/><br>
<input type="submit">
</form>
`, requestedScopes)))
if err != nil {
log.Error().Err(err).Msg("Error writing response")
oauth2.WriteAuthorizeError(w, ar, err)
s.oauth2.WriteAuthorizeError(w, ar, err)
}
return
}
// use reva sepcific implementation that uses existing auth managers
if err := store.Authenticate(ctx, username, password); err != nil {
oauth2.WriteAuthorizeError(w, ar, err)
actx, err := s.authmgr.Authenticate(ctx, username, password)
if err != nil {
s.oauth2.WriteAuthorizeError(w, ar, err)
}
uid, ok := user.ContextGetUserID(actx)
if !ok {
// try to look up user by username
// TODO log warning or should we fail?
uid = &typespb.UserId{
OpaqueId: username,
}
}

// let's see what scopes the user gave consent to
Expand All @@ -84,7 +95,7 @@ func (s *svc) doAuth(w http.ResponseWriter, r *http.Request) {
}

// Now that the user is authorized, we set up a session:
mySessionData := newSession(username, getSub(ctx, username))
mySessionData := newSession(username, uid)

// When using the HMACSHA strategy you must use something that implements the HMACSessionContainer.
// It brings you the power of overriding the default values.
Expand All @@ -110,18 +121,18 @@ func (s *svc) doAuth(w http.ResponseWriter, r *http.Request) {
// Now we need to get a response. This is the place where the AuthorizeEndpointHandlers kick in and start processing the request.
// NewAuthorizeResponse is capable of running multiple response type handlers which in turn enables this library
// to support open id connect.
response, err := oauth2.NewAuthorizeResponse(ctx, ar, mySessionData)
response, err := s.oauth2.NewAuthorizeResponse(ctx, ar, mySessionData)

// Catch any errors, e.g.:
// * unknown client
// * invalid redirect
// * ...
if err != nil {
log.Error().Err(err).Msg("Error occurred in NewAuthorizeResponse")
oauth2.WriteAuthorizeError(w, ar, err)
s.oauth2.WriteAuthorizeError(w, ar, err)
return
}

// Last but not least, send the response!
oauth2.WriteAuthorizeResponse(w, ar, response)
s.oauth2.WriteAuthorizeResponse(w, ar, response)
}
3 changes: 3 additions & 0 deletions cmd/revad/svcs/httpsvcs/oidcprovider/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ func (s *svc) doHome(w http.ResponseWriter, r *http.Request) {
<a href="%s">Make an invalid request</a>
</li>
</ul>`,
// TODO(jfd): make sure phoenix uses random state and nonce, see https://tools.ietf.org/html/rfc6819#section-4.4.1.8
// - nonce vs jti https://security.stackexchange.com/a/188171
// - state vs nonce https://stackoverflow.com/a/46859861
clientConf.AuthCodeURL("some-random-state-foobar")+"&nonce=some-random-nonce",
"http://localhost:9998/oauth2/auth?client_id=my-client&redirect_uri=http%3A%2F%2Flocalhost%3A9998%2Fcallback&response_type=token%20id_token&scope=fosite%20openid&state=some-random-state-foobar&nonce=some-random-nonce",
clientConf.AuthCodeURL("some-random-state-foobar")+"&nonce=some-random-nonce",
Expand Down
6 changes: 3 additions & 3 deletions cmd/revad/svcs/httpsvcs/oidcprovider/introspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ import (
func (s *svc) doIntrospect(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
ir, err := oauth2.NewIntrospectionRequest(ctx, r, emptySession())
ir, err := s.oauth2.NewIntrospectionRequest(ctx, r, emptySession())
if err != nil {
log.Error().Err(err).Msg("Error occurred in NewIntrospectionRequest")
oauth2.WriteIntrospectionError(w, err)
s.oauth2.WriteIntrospectionError(w, err)
return
}

oauth2.WriteIntrospectionResponse(w, ir)
s.oauth2.WriteIntrospectionResponse(w, ir)
}
142 changes: 78 additions & 64 deletions cmd/revad/svcs/httpsvcs/oidcprovider/oidcprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
package oidcprovider

import (
"context"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"encoding/hex"
"fmt"
"net/http"
"time"

Expand All @@ -33,9 +31,14 @@ import (
"github.com/ory/fosite/storage"
"github.com/ory/fosite/token/jwt"

typespb "github.com/cs3org/go-cs3apis/cs3/types"
"github.com/cs3org/reva/cmd/revad/httpserver"
"github.com/cs3org/reva/cmd/revad/svcs/httpsvcs"
"github.com/cs3org/reva/pkg/appctx"
"github.com/cs3org/reva/pkg/auth"
authmgr "github.com/cs3org/reva/pkg/auth/manager/registry"
"github.com/cs3org/reva/pkg/user"
usermgr "github.com/cs3org/reva/pkg/user/manager/registry"
"github.com/mitchellh/mapstructure"
)

Expand All @@ -44,20 +47,26 @@ func init() {
}

type config struct {
Prefix string `mapstructure:"prefix"`
Prefix string `mapstructure:"prefix"`
AuthManager string `mapstructure:"auth_manager"`
AuthManagers map[string]map[string]interface{} `mapstructure:"auth_managers"`
UserManager string `mapstructure:"user_manager"`
UserManagers map[string]map[string]interface{} `mapstructure:"user_managers"`
}

type svc struct {
prefix string
handler http.Handler
authmgr auth.Manager
usermgr user.Manager
store *storage.MemoryStore
oauth2 fosite.OAuth2Provider
}

// This is an exemplary storage instance. We will add a client and a user to it so we can use these later on.
var store = newExampleStore()

func newExampleStore() *storage.MemoryStore {
return &storage.MemoryStore{
IDSessions: make(map[string]fosite.Requester),
// TODO(jfd): read clients from a json file
Clients: map[string]fosite.Client{
"phoenix": &fosite.DefaultClient{
ID: "phoenix",
Expand All @@ -75,21 +84,6 @@ func newExampleStore() *storage.MemoryStore {
Scopes: []string{"openid", "profile", "email", "offline"},
},
},
// TODO implement reva specific user store that uses existing user managers
Users: map[string]storage.MemoryUserRelation{
"aaliyah_abernathy": {
Username: "aaliyah_abernathy",
Password: "secret",
},
"aaliyah_adams": {
Username: "aaliyah_adams",
Password: "secret",
},
"aaliyah_anderson": {
Username: "aaliyah_anderson",
Password: "secret",
},
},
AuthorizeCodes: map[string]storage.StoreAuthorizeCode{},
Implicit: map[string]fosite.Requester{},
AccessTokens: map[string]fosite.Requester{},
Expand All @@ -107,35 +101,13 @@ var fconfig = new(compose.Config)
var start = compose.CommonStrategy{
// alternatively you could use:
// OAuth2Strategy: compose.NewOAuth2JWTStrategy(mustRSAKey())
// TODO(jfd): generate / read proper secret from config
CoreStrategy: compose.NewOAuth2HMACStrategy(fconfig, []byte("some-super-cool-secret-that-nobody-knows"), nil),

// open id connect strategy
OpenIDConnectTokenStrategy: compose.NewOpenIDConnectStrategy(fconfig, mustRSAKey()),
}

var oauth2 = compose.Compose(
fconfig,
store,
start,
nil,

// enabled handlers
compose.OAuth2AuthorizeExplicitFactory,
compose.OAuth2AuthorizeImplicitFactory,
compose.OAuth2ClientCredentialsGrantFactory,
compose.OAuth2RefreshTokenGrantFactory,
compose.OAuth2ResourceOwnerPasswordCredentialsFactory,

compose.OAuth2TokenRevocationFactory,
compose.OAuth2TokenIntrospectionFactory,

// be aware that open id connect factories need to be added after oauth2 factories to work properly.
compose.OpenIDConnectExplicitFactory,
compose.OpenIDConnectImplicitFactory,
compose.OpenIDConnectHybridFactory,
compose.OpenIDConnectRefreshFactory,
)

// A session is passed from the `/auth` to the `/token` endpoint. You probably want to store data like: "Who made the request",
// "What organization does that person belong to" and so on.
// For our use case, the session will meet the requirements imposed by JWT access tokens, HMAC access tokens and OpenID Connect
Expand All @@ -146,11 +118,11 @@ var oauth2 = compose.Compose(
// Usually, you could do:
//
// session = new(fosite.DefaultSession)
func newSession(username string, sub string) *openid.DefaultSession {
func newSession(username string, uid *typespb.UserId) *openid.DefaultSession {
return &openid.DefaultSession{
Claims: &jwt.IDTokenClaims{
Issuer: "http://localhost:9998",
Subject: sub,
Issuer: uid.Idp,
Subject: uid.OpaqueId,
//Audience: []string{"https://my-client.my-application.com"},
ExpiresAt: time.Now().Add(time.Hour * 6),
IssuedAt: time.Now(),
Expand All @@ -160,45 +132,87 @@ func newSession(username string, sub string) *openid.DefaultSession {
Headers: &jwt.Headers{
Extra: make(map[string]interface{}),
},
Subject: sub,
Subject: uid.OpaqueId,
Username: username,
}
}

// emptySession creates a session object and fills it with safe defaults
func emptySession() *openid.DefaultSession {
return newSession("", "")
return newSession("", &typespb.UserId{})
}

func mustRSAKey() *rsa.PrivateKey {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
// TODO really panic?
// TODO(jfd): don't panic!
panic(err)
}
return key
}

// TODO currently we fake a sub. it would change when tha username changes ...
func getSub(ctx context.Context, username string) string {
hasher := md5.New()
_, err := hasher.Write([]byte(username))
if err != nil {
appctx.GetLogger(ctx).Error().Err(err).Msg("Error occurred in getSub")
return ""
func getAuthManager(manager string, m map[string]map[string]interface{}) (auth.Manager, error) {
if f, ok := authmgr.NewFuncs[manager]; ok {
return f(m[manager])
}

return nil, fmt.Errorf("driver %s not found for auth manager", manager)
}

func getUserManager(manager string, m map[string]map[string]interface{}) (user.Manager, error) {
if f, ok := usermgr.NewFuncs[manager]; ok {
return f(m[manager])
}
return hex.EncodeToString(hasher.Sum(nil))

return nil, fmt.Errorf("driver %s not found for user manager", manager)
}

// New returns a new webuisvc
// New returns a new oidcprovidersvc
func New(m map[string]interface{}) (httpsvcs.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
return nil, err
}

authManager, err := getAuthManager(c.AuthManager, c.AuthManagers)
if err != nil {
return nil, err
}

userManager, err := getUserManager(c.UserManager, c.UserManagers)
if err != nil {
return nil, err
}

store := newExampleStore()
s := &svc{
prefix: conf.Prefix,
prefix: c.Prefix,
authmgr: authManager,
usermgr: userManager,
// This is an exemplary storage instance. We will add a client and a user to it so we can use these later on.
store: store,
oauth2: compose.Compose(
fconfig,
store,
start,
nil,

// enabled handlers
compose.OAuth2AuthorizeExplicitFactory,
compose.OAuth2AuthorizeImplicitFactory,
compose.OAuth2ClientCredentialsGrantFactory,
compose.OAuth2RefreshTokenGrantFactory,
compose.OAuth2ResourceOwnerPasswordCredentialsFactory,

compose.OAuth2TokenRevocationFactory,
compose.OAuth2TokenIntrospectionFactory,

// be aware that open id connect factories need to be added after oauth2 factories to work properly.
compose.OpenIDConnectExplicitFactory,
compose.OpenIDConnectImplicitFactory,
compose.OpenIDConnectHybridFactory,
compose.OpenIDConnectRefreshFactory,
),
}
s.setHandler()
return s, nil
Expand Down Expand Up @@ -240,7 +254,7 @@ func (s *svc) setHandler() {
case "userinfo":
s.doUserinfo(w, r)
case "sessions":
// TODO only for development
// TODO(jfd) make session lookup configurable? only for development?
s.doSessions(w, r)
default:
w.WriteHeader(http.StatusNotFound)
Expand Down

0 comments on commit ca23166

Please sign in to comment.