bricks: github.com/pace/bricks/http/oauth2 Index | Examples | Files | Directories

package oauth2

import "github.com/pace/bricks/http/oauth2"

Package oauth2 provides a middelware that introspects the auth token on behalf of PACE services and populate the request context with useful information when the token is valid, otherwise aborts the request.

Code:

package main

import (
    "context"
    "errors"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/gorilla/mux"
    "github.com/pace/bricks/http/security"
    "github.com/pace/bricks/maintenance/log"
)

type tokenInspectorWithError struct {
    returnedErr error
}

func (t *tokenInspectorWithError) IntrospectToken(ctx context.Context, token string) (*IntrospectResponse, error) {
    return nil, t.returnedErr
}

func TestHandlerIntrospectErrorAsMiddleware(t *testing.T) {
    testCases := []struct {
        desc         string
        returnedErr  error
        expectedCode int
        expectedBody string
    }{
        {
            desc:         "token introspecter returns ErrBadUpstreamResponse",
            returnedErr:  ErrBadUpstreamResponse,
            expectedCode: 502,
            expectedBody: "bad upstream response when introspecting token\n",
        },
        {
            desc:         "token introspecter returns ErrUpstreamConnection",
            returnedErr:  ErrUpstreamConnection,
            expectedCode: 502,
            expectedBody: "problem connecting to the introspection endpoint\n",
        },
        {
            desc:         "token introspecter returns ErrInvalidToken",
            returnedErr:  ErrInvalidToken,
            expectedCode: 401,
            expectedBody: "user token is invalid\n",
        },
    }
    for _, tC := range testCases {
        t.Run(tC.desc, func(t *testing.T) {
            m := NewMiddleware(&tokenInspectorWithError{returnedErr: tC.returnedErr})
            r := mux.NewRouter()
            r.Use(m.Handler)
            r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {})

            req := httptest.NewRequest("GET", "/", nil)
            req.Header.Set("Authorization", "Bearer some-token")

            w := httptest.NewRecorder()
            r.ServeHTTP(w, req)

            resp := w.Result()
            body, err := ioutil.ReadAll(resp.Body)
            resp.Body.Close()
            if err != nil {
                t.Fatal(err)
            }

            if got, ex := resp.StatusCode, tC.expectedCode; got != ex {
                t.Errorf("Expected status code %d, got %d", ex, got)
            }

            if got, ex := string(body), tC.expectedBody; got != ex {
                t.Errorf("Expected body %q, got %q", ex, got)
            }
        })
    }
}

type tokenIntrospectedSuccessful struct {
    response *IntrospectResponse
}

func (t *tokenIntrospectedSuccessful) IntrospectToken(ctx context.Context, token string) (*IntrospectResponse, error) {
    return t.response, nil
}

func TestAuthenticatorWithSuccess(t *testing.T) {
    testCases := []struct {
        desc           string
        userScopes     string
        expectedScopes string
        active         bool
        clientId       string
        userId         string
    }{
        {desc: "Tests a valid Request with OAuth2 Authentication without Scope checking",
            active:     true,
            userScopes: "ABC DHHG kjdk",
            clientId:   "ClientId",
            userId:     "UserId",
        },
        {desc: "Tests a valid Request with OAuth2 Authentication and one scope to check",
            active:         true,
            userScopes:     "ABC DHHG kjdk",
            clientId:       "ClientId",
            userId:         "UserId",
            expectedScopes: "ABC",
        },
        {desc: "Tests a valid Request with OAuth2 Authentication and two scope to check",
            active:         true,
            userScopes:     "ABC DHHG kjdk",
            clientId:       "ClientId",
            userId:         "UserId",
            expectedScopes: "ABC kjdk",
        },
    }
    for _, tC := range testCases {
        t.Run(tC.desc, func(t *testing.T) {
            w := httptest.NewRecorder()
            r := httptest.NewRequest("GET", "/", nil)
            r.Header.Add("Authorization", "Bearer bearer")

            auth := NewAuthorizer(&tokenIntrospectedSuccessful{&IntrospectResponse{
                Active:   tC.active,
                Scope:    tC.userScopes,
                ClientID: tC.clientId,
                UserID:   tC.userId,
            }}, &Config{})
            if tC.expectedScopes != "" {
                auth = auth.WithScope(tC.expectedScopes)
            }
            authorize, b := auth.Authorize(r, w)
            resp := w.Result()
            body, err := ioutil.ReadAll(resp.Body)
            resp.Body.Close()
            if err != nil {
                t.Fatal(err)
            }
            if !b || authorize == nil {
                t.Errorf("Expected succesfull Authentication, but was not succesfull with code %d and body %q", resp.StatusCode, string(body))
                return
            }
            to, _ := security.GetTokenFromContext(authorize)
            tok, ok := to.(*token)

            if !ok || tok.value != "bearer" || tok.scope != Scope(tC.userScopes) || tok.clientID != tC.clientId || tok.userID != tC.userId {
                t.Errorf("Expected %v but got %v", auth.introspection.(*tokenIntrospectedSuccessful).response, tok)
            }
        })
    }
}

func TestAuthenticationSuccessScopeError(t *testing.T) {
    auth := NewAuthorizer(&tokenIntrospectedSuccessful{&IntrospectResponse{
        Active:   true,
        Scope:    "ABC DEF DFE",
        ClientID: "ClientId",
        UserID:   "UserId",
    }}, &Config{}).WithScope("DE")

    w := httptest.NewRecorder()
    r := httptest.NewRequest("GET", "/", nil)
    r.Header.Add("Authorization", "Bearer bearer")

    _, b := auth.Authorize(r, w)

    resp := w.Result()
    body, err := ioutil.ReadAll(resp.Body)
    defer resp.Body.Close()
    if err != nil {
        t.Fatal(err)
    }
    if b {
        t.Errorf("Expected error in Authentication, but was succesfull with code %d and body %v", resp.StatusCode, string(body))
    }
    if got, ex := w.Code, http.StatusForbidden; got != ex {
        t.Errorf("Expected status code %d, got %d", ex, got)
    }
    if got, ex := string(body), "Forbidden - requires scope \"DE\"\n"; got != ex {
        t.Errorf("Expected status code %q, got %q", ex, got)
    }
}

func TestAuthenticationWithErrors(t *testing.T) {
    testCases := []struct {
        desc         string
        returnedErr  error
        expectedCode int
        expectedBody string
    }{
        {
            desc:         "token introspecter returns ErrBadUpstreamResponse",
            returnedErr:  ErrBadUpstreamResponse,
            expectedCode: http.StatusBadGateway,
            expectedBody: "bad upstream response when introspecting token\n",
        },
        {
            desc:         "token introspecter returns ErrUpstreamConnection",
            returnedErr:  ErrUpstreamConnection,
            expectedCode: http.StatusBadGateway,
            expectedBody: "problem connecting to the introspection endpoint\n",
        },
        {
            desc:         "token introspecter returns ErrInvalidToken",
            returnedErr:  ErrInvalidToken,
            expectedCode: http.StatusUnauthorized,
            expectedBody: "user token is invalid\n",
        },
        {
            desc:         "token introspecter returns any other error",
            returnedErr:  errors.New("any other error"),
            expectedCode: http.StatusInternalServerError,
            expectedBody: "any other error\n",
        },
    }
    for _, tC := range testCases {
        t.Run(tC.desc, func(t *testing.T) {
            auth := NewAuthorizer(&tokenInspectorWithError{returnedErr: tC.returnedErr}, &Config{})
            w := httptest.NewRecorder()
            r := httptest.NewRequest("GET", "/", nil)
            r.Header.Add("Authorization", "Bearer bearer")
            _, b := auth.Authorize(r, w)

            resp := w.Result()
            body, err := ioutil.ReadAll(resp.Body)
            defer resp.Body.Close()
            if err != nil {
                t.Fatal(err)
            }
            if b {
                t.Errorf("Expected error in authentication, but was succesful with code %d and body %v", resp.StatusCode, string(body))
            }

            if got, ex := w.Code, tC.expectedCode; got != ex {
                t.Errorf("Expected status code %d, got %d", ex, got)
            }

            if string(body) != tC.expectedBody {
                t.Errorf("Expected body %q, got %q", string(body), tC.expectedBody)
            }
        })
    }
}

func main() {
    r := mux.NewRouter()
    middleware := Middleware{}

    r.Use(middleware.Handler)

    r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        userid, _ := UserID(r.Context())
        log.Printf("AUDIT: User %s does something", userid)

        if HasScope(r.Context(), "dtc:codes:write") {
            _, err := fmt.Fprintf(w, "User has scope.")
            if err != nil {
                panic(err)
            }
            return
        }
        _, err := fmt.Fprintf(w, "Your client may not have the right scopes to see the secret code")
        if err != nil {
            panic(err)
        }
    })

    srv := &http.Server{
        Handler: r,
        Addr:    "127.0.0.1:8000",
    }

    log.Fatal(srv.ListenAndServe())
}

func TestRequest(t *testing.T) {
    var to = token{
        value:    "somevalue",
        userID:   "someuserid",
        clientID: "someclientid",
        scope:    Scope("scope1 scope2"),
    }

    r := httptest.NewRequest("GET", "http://example.com", nil)
    ctx := security.ContextWithToken(r.Context(), &to)
    r = r.WithContext(ctx)

    r2 := Request(r)
    header := r2.Header.Get("Authorization")

    if header != "Bearer somevalue" {
        t.Fatalf("Expected request to have authorization header, got: %v", header)
    }
}

func TestRequestWithNoToken(t *testing.T) {
    r := httptest.NewRequest("GET", "http://example.com", nil)
    r2 := Request(r)
    header := r2.Header.Get("Authorization")

    if header != "" {
        t.Fatalf("Expected request to have no authorization header, got: %v", header)
    }
}

func TestSuccessfulAccessors(t *testing.T) {
    expectedBearerToken := "somevalue"
    expectedUserID := "someuserid"
    expectedClientID := "someclientid"
    expectedBackend := "some-backend"
    expectedScopes := Scope("scope1 scope2")

    var to = token{
        value:    expectedBearerToken,
        userID:   expectedUserID,
        clientID: expectedClientID,
        scope:    expectedScopes,
        backend:  expectedBackend,
    }

    ctx := security.ContextWithToken(context.TODO(), &to)
    newCtx := context.TODO()
    ctx = ContextTransfer(ctx, newCtx)

    uid, _ := UserID(ctx)
    cid, _ := ClientID(ctx)
    backend, _ := Backend(ctx)
    bearerToken, ok := security.GetTokenFromContext(ctx)
    scopes := Scopes(ctx)
    hasScope := HasScope(ctx, "scope2")

    if uid != expectedUserID {
        t.Fatalf("Expected %v, got: %v", expectedUserID, uid)
    }

    if cid != expectedClientID {
        t.Fatalf("Expected %v, got: %v", expectedClientID, cid)
    }

    if backend != expectedBackend {
        t.Fatalf("Expected %v, got: %v", expectedBackend, backend)
    }

    if !ok || bearerToken.GetValue() != expectedBearerToken {
        t.Fatalf("Expected %v, got: %v", expectedBearerToken, bearerToken)
    }

    if scopes[0] != "scope1" || scopes[1] != "scope2" {
        t.Fatalf("Expected %v, got: %v", expectedScopes, scopes)
    }

    if !hasScope {
        t.Fatalf("Expected true, got: %v", hasScope)
    }
}

// Ensure we return sensible results when no data is present, and not panic.
func TestUnsuccessfulAccessors(t *testing.T) {
    ctx := context.TODO()

    uid, uidOK := UserID(ctx)
    cid, cidOK := ClientID(ctx)
    backend, backendOK := Backend(ctx)
    bt, ok := security.GetTokenFromContext(ctx)
    scopes := Scopes(ctx)
    hasScope := HasScope(ctx, "scope2")

    if uid != "" || uidOK {
        t.Fatalf("Expected no %v, got: %v", "UserID", uid)
    }

    if cid != "" || cidOK {
        t.Fatalf("Expected no %v, got: %v", "ClientID", cid)
    }

    if backend != nil || backendOK {
        t.Fatalf("Expected no %v, got: %v", "Backend", backend)
    }

    if ok || bt != nil {
        t.Fatalf("Expected no %v, got: %v", "BearerToken", bt)
    }

    if len(scopes) > 0 {
        t.Fatalf("Expected no scopes, got: %v", scopes)
    }

    if hasScope {
        t.Fatalf("Expected hasScope to return false, got: %v", hasScope)
    }
}

func TestWithBearerToken(t *testing.T) {
    ctx := context.Background()
    ctx = WithBearerToken(ctx, "some access token")
    token, ok := security.GetTokenFromContext(ctx)
    if !ok || token.GetValue() != "some access token" {
        t.Error("could not store bearer token in context")
    }
}

Code:

package main

import (
    "context"
    "fmt"
    "net/http/httptest"

    "github.com/pace/bricks/http/oauth2"
)

var _ oauth2.TokenIntrospecter = (*multiAuthBackends)(nil)

type multiAuthBackends []oauth2.TokenIntrospecter

func (b multiAuthBackends) IntrospectToken(ctx context.Context, token string) (resp *oauth2.IntrospectResponse, err error) {
    for _, backend := range b {
        resp, err = backend.IntrospectToken(ctx, token)
        if resp != nil && err == nil {
            return
        }
    }
    return nil, oauth2.ErrInvalidToken
}

type authBackend [2]string

func (b *authBackend) IntrospectToken(ctx context.Context, token string) (*oauth2.IntrospectResponse, error) {
    if b[1] == token {
        return &oauth2.IntrospectResponse{
            Active:  true,
            Backend: b,
        }, nil
    }
    return nil, oauth2.ErrInvalidToken
}

func main() {
    // In case you have multiple authorization backends, you can use the
    // oauth2.Backend(context.Context) function to retrieve the backend that
    // authorized the request. The actual value used for the backend depends on
    // your implementation: you can use constants or pointers, like in this
    // example.

    authorizer := oauth2.NewAuthorizer(multiAuthBackends{
        &authBackend{"A", "token-a"},
        &authBackend{"B", "token-b"},
        &authBackend{"C", "token-c"},
    }, nil)

    r := httptest.NewRequest("GET", "/some/endpoint", nil)
    r.Header.Set("Authorization", "Bearer token-b")

    if authorizer.CanAuthorizeRequest(r) {
        ctx, ok := authorizer.Authorize(r, nil)
        usedBackend, _ := oauth2.Backend(ctx)
        fmt.Printf("%t %s", ok, usedBackend.(*authBackend)[0])
    }

}

Index

Examples

Package Files

authorizer.go introspection.go oauth2.go scope.go

Variables

var ErrBadUpstreamResponse = errors.New("bad upstream response when introspecting token")

ErrBadUpstreamResponse the response from the server has the wrong format

var ErrInvalidToken = errors.New("user token is invalid")

ErrInvalidToken in case the token is not valid or expired

var ErrUpstreamConnection = errors.New("problem connecting to the introspection endpoint")

ErrUpstreamConnection connection issue

func Backend Uses

func Backend(ctx context.Context) (interface{}, bool)

Backend returns the backend stored in the context. It identifies the authorization backend for the token.

func BearerToken Uses

func BearerToken(ctx context.Context) (string, bool)

Deprecated: BearerToken was moved to the security package, because it's used by apiKey and oauth2 authorization. BearerToken returns the bearer token stored in ctx

func ClientID Uses

func ClientID(ctx context.Context) (string, bool)

ClientID returns the clientID stored in ctx

func ContextTransfer Uses

func ContextTransfer(sourceCtx context.Context, targetCtx context.Context) context.Context

ContextTransfer sources the oauth2 token from the sourceCtx and returning a new context based on the targetCtx

func HasScope Uses

func HasScope(ctx context.Context, scope Scope) bool

HasScope extracts an access token from context and checks if the permissions represented by the provided scope are included in the valid scope.

func Request Uses

func Request(r *http.Request) *http.Request

Request adds Authorization token to r

func Scopes Uses

func Scopes(ctx context.Context) []string

Scopes returns the scopes stored in ctx

func UserID Uses

func UserID(ctx context.Context) (string, bool)

UserID returns the userID stored in ctx

func WithBearerToken Uses

func WithBearerToken(ctx context.Context, bearerToken string) context.Context

Deprecated: WithBearerToken was moved to the security package, because it's used by api key and oauth2 authorization. returns a new context with the given bearer token Use security.BearerToken() to retrieve the token. Use Request() to obtain a request with the Authorization header set accordingly.

type Authorizer Uses

type Authorizer struct {
    // contains filtered or unexported fields
}

Authorizer is an implementation of security.Authorizer for OAuth2 it uses introspection to get user data and can check the scope

func NewAuthorizer Uses

func NewAuthorizer(introspector TokenIntrospecter, cfg *Config) *Authorizer

NewAuthorizer creates an Authorizer for a specific TokenIntrospecter This Authorizer does not check the scope

func (*Authorizer) Authorize Uses

func (a *Authorizer) Authorize(r *http.Request, w http.ResponseWriter) (context.Context, bool)

Authorize authorizes a request with an introspection and validates the scope Success: returns context with the introspection result and true Error: writes all errors directly to response, returns unchanged context and false

func (*Authorizer) CanAuthorizeRequest Uses

func (a *Authorizer) CanAuthorizeRequest(r *http.Request) bool

CanAuthorizeRequest returns true, if the request contains a token in the configured header, otherwise false

func (*Authorizer) WithScope Uses

func (a *Authorizer) WithScope(tok string) *Authorizer

WithScope returns a new Authorizer with the same TokenIntrospecter and the same Config that also checks the scope of a request

type Config Uses

type Config struct {
    Description       string
    Implicit          *Flow
    Password          *Flow
    ClientCredentials *Flow
    AuthorizationCode *Flow
}

Config contains the configuration from the api definition - currently not used

type Flow Uses

type Flow struct {
    AuthorizationURL string
    TokenURL         string
    RefreshURL       string
    Scopes           map[string]string
}

Flow is a part of the OAuth2 config from the security schema

type IntrospectResponse Uses

type IntrospectResponse struct {
    Active   bool   `json:"active"`
    Scope    string `json:"scope"`
    ClientID string `json:"client_id"`
    UserID   string `json:"user_id"`

    // Backend identifies the backend used for introspection. This attribute
    // exists as a convenience if you have more than one authorization backend
    // and need to distinguish between those.
    Backend interface{} `json:"-"`
}

IntrospectResponse in case of a successful check of the oauth2 request

type Middleware Uses

type Middleware struct {
    Backend TokenIntrospecter
}

Deprecated: Middleware holds data necessary for Oauth processing - Deprecated for generated apis, use the generated Authentication Backend of the API with oauth2.Authorizer

func NewMiddleware Uses

func NewMiddleware(backend TokenIntrospecter) *Middleware

Deprecated: NewMiddleware creates a new Oauth middleware - Deprecated for generated apis, use the generated AuthenticationBackend of the API with oauth2.Authorizer

func (*Middleware) Handler Uses

func (m *Middleware) Handler(next http.Handler) http.Handler

Handler will parse the bearer token, introspect it, and put the token and other relevant information back in the context.

type Scope Uses

type Scope string

Scope represents an OAuth 2 access token scope

func (*Scope) IsIncludedIn Uses

func (s *Scope) IsIncludedIn(t Scope) bool

IsIncludedIn checks if the permissions of a scope s are also included in the provided scope t. This can be useful to check if a scope has all required permissions to access an endpoint.

type TokenIntrospecter Uses

type TokenIntrospecter interface {
    IntrospectToken(ctx context.Context, token string) (*IntrospectResponse, error)
}

TokenIntrospecter needs to be implemented for token lookup

Directories

PathSynopsis
middleware

Package oauth2 imports 9 packages (graph) and is imported by 4 packages. Updated 2020-04-29. Refresh now. Tools for package owners.