Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
390 lines
8.5 KiB
Go
390 lines
8.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/cortezaproject/corteza/server/auth/request"
|
|
"github.com/cortezaproject/corteza/server/auth/settings"
|
|
"github.com/cortezaproject/corteza/server/pkg/auth"
|
|
"github.com/cortezaproject/corteza/server/system/types"
|
|
"github.com/go-chi/jwtauth"
|
|
oauth2models "github.com/go-oauth2/oauth2/v4/models"
|
|
"github.com/lestrrat-go/jwx/jwt"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func Test_oauth2AuthorizeSuccess(t *testing.T) {
|
|
var (
|
|
user = makeMockUser()
|
|
|
|
req = &http.Request{
|
|
Form: url.Values{},
|
|
PostForm: url.Values{},
|
|
}
|
|
|
|
oauthService oauth2Service
|
|
authService authService
|
|
authHandlers *AuthHandlers
|
|
authReq *request.AuthReq
|
|
)
|
|
|
|
tcc := []testingExpect{
|
|
{
|
|
name: "authorization success",
|
|
payload: -1,
|
|
err: "",
|
|
template: "",
|
|
fn: func(_ *settings.Settings) {
|
|
oauthService = &oauth2ServiceMocked{
|
|
handleAuthorizeRequest: func(w http.ResponseWriter, r *http.Request) error {
|
|
return nil
|
|
},
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "authorization failure",
|
|
payload: http.StatusInternalServerError,
|
|
err: "not authorized",
|
|
template: TmplInternalError,
|
|
fn: func(_ *settings.Settings) {
|
|
oauthService = &oauth2ServiceMocked{
|
|
handleAuthorizeRequest: func(w http.ResponseWriter, r *http.Request) error {
|
|
return fmt.Errorf("not authorized")
|
|
},
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcc {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rq := require.New(t)
|
|
|
|
authSettings := &settings.Settings{}
|
|
|
|
tc.fn(authSettings)
|
|
|
|
authHandlers = &AuthHandlers{
|
|
Log: zap.NewNop(),
|
|
AuthService: authService,
|
|
Settings: authSettings,
|
|
OAuth2: oauthService,
|
|
}
|
|
authReq = prepareClientAuthReq(authHandlers, req, user)
|
|
|
|
err := authHandlers.oauth2Authorize(authReq)
|
|
|
|
rq.NoError(err)
|
|
rq.Equal(tc.template, authReq.Template)
|
|
rq.Equal(tc.payload, authReq.Status)
|
|
|
|
if tc.err != "" {
|
|
rq.EqualError(errors.New(tc.err), authReq.Data["error"].(error).Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_oauth2AuthorizeSuccessSetParams(t *testing.T) {
|
|
var (
|
|
user = makeMockUser()
|
|
|
|
req = &http.Request{
|
|
Form: url.Values{},
|
|
PostForm: url.Values{},
|
|
}
|
|
|
|
authService authService
|
|
authHandlers *AuthHandlers
|
|
authReq *request.AuthReq
|
|
|
|
authSettings = &settings.Settings{}
|
|
|
|
rq = require.New(t)
|
|
)
|
|
|
|
oauthService := &oauth2ServiceMocked{
|
|
handleAuthorizeRequest: func(w http.ResponseWriter, r *http.Request) error {
|
|
return nil
|
|
},
|
|
}
|
|
|
|
authHandlers = &AuthHandlers{
|
|
Log: zap.NewNop(),
|
|
AuthService: authService,
|
|
Settings: authSettings,
|
|
OAuth2: oauthService,
|
|
}
|
|
authReq = prepareClientAuthReq(authHandlers, req, user)
|
|
authReq.Session.Values["oauth2AuthParams"] = url.Values{"foo": []string{"bar"}}
|
|
|
|
err := authHandlers.oauth2Authorize(authReq)
|
|
|
|
rq.NoError(err)
|
|
rq.Equal("", authReq.Template)
|
|
rq.Equal(-1, authReq.Status)
|
|
rq.Equal(nil, authReq.Data["error"])
|
|
}
|
|
|
|
func Test_generateIdToken(t *testing.T) {
|
|
var (
|
|
req = require.New(t)
|
|
)
|
|
|
|
signed, err := generateIdToken(
|
|
&types.User{},
|
|
&types.AuthClient{
|
|
Secret: "correct horse battery stable",
|
|
},
|
|
&oauth2models.Token{},
|
|
"http://cortezaproject.org",
|
|
)
|
|
|
|
req.NoError(err)
|
|
req.NotEmpty(signed)
|
|
}
|
|
|
|
func Test_generateIdTokenClaims(t *testing.T) {
|
|
var (
|
|
updatedAt = time.Now()
|
|
|
|
user = &types.User{
|
|
ID: 42,
|
|
Name: "John Doe",
|
|
Handle: "jdoe",
|
|
Email: "john@example.tld",
|
|
EmailConfirmed: true,
|
|
UpdatedAt: &updatedAt,
|
|
Meta: &types.UserMeta{PreferredLanguage: "sl"},
|
|
}
|
|
|
|
client = &types.AuthClient{ID: 1, Secret: "correct horse battery stable"}
|
|
)
|
|
|
|
tcc := []struct {
|
|
name string
|
|
scope string
|
|
present []string
|
|
absent []string
|
|
}{
|
|
{
|
|
name: "without profile scope",
|
|
scope: "openid",
|
|
present: []string{"sub", "user_id", "email", "email_verified", "iat", "aud"},
|
|
absent: []string{"name", "preferred_username", "locale", "updated_at"},
|
|
},
|
|
{
|
|
name: "with profile scope",
|
|
scope: "openid profile",
|
|
present: []string{"sub", "email", "name", "preferred_username", "locale", "updated_at"},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcc {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var (
|
|
req = require.New(t)
|
|
ti = &oauth2models.Token{}
|
|
)
|
|
|
|
ti.SetScope(tc.scope)
|
|
|
|
signed, err := generateIdToken(user, client, ti, "https://example.tld/auth")
|
|
req.NoError(err)
|
|
|
|
token, err := jwt.Parse(signed)
|
|
req.NoError(err)
|
|
|
|
for _, claim := range tc.present {
|
|
_, has := token.Get(claim)
|
|
req.True(has, "expecting claim %q", claim)
|
|
}
|
|
|
|
for _, claim := range tc.absent {
|
|
_, has := token.Get(claim)
|
|
req.False(has, "not expecting claim %q", claim)
|
|
}
|
|
|
|
req.Equal("https://example.tld/auth", token.Issuer())
|
|
req.Equal("42", token.Subject())
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_oauth2Userinfo(t *testing.T) {
|
|
var (
|
|
updatedAt = time.Now()
|
|
|
|
user = &types.User{
|
|
ID: 42,
|
|
Name: "John Doe",
|
|
Handle: "jdoe",
|
|
Email: "john@example.tld",
|
|
EmailConfirmed: true,
|
|
UpdatedAt: &updatedAt,
|
|
Meta: &types.UserMeta{PreferredLanguage: "sl"},
|
|
}
|
|
)
|
|
|
|
// swap the global token issuer for one we can sign test tokens with
|
|
original := auth.TokenIssuer
|
|
t.Cleanup(func() { auth.TokenIssuer = original })
|
|
|
|
issuer, err := auth.NewTokenIssuer(
|
|
auth.WithSecretSigner("correct horse battery stable"),
|
|
auth.WithLookup(func(ctx context.Context, tokenID string) error { return nil }),
|
|
)
|
|
require.NoError(t, err)
|
|
auth.TokenIssuer = issuer
|
|
|
|
tcc := []struct {
|
|
name string
|
|
scope string
|
|
noToken bool
|
|
notFound bool
|
|
status int
|
|
claims map[string]interface{}
|
|
absent []string
|
|
}{
|
|
{
|
|
name: "openid only",
|
|
scope: "openid",
|
|
status: http.StatusOK,
|
|
claims: map[string]interface{}{"sub": "42"},
|
|
absent: []string{"name", "preferred_username", "locale", "email", "email_verified"},
|
|
},
|
|
{
|
|
name: "profile scope",
|
|
scope: "openid profile",
|
|
status: http.StatusOK,
|
|
claims: map[string]interface{}{
|
|
"sub": "42",
|
|
"name": "John Doe",
|
|
"preferred_username": "jdoe",
|
|
"locale": "sl",
|
|
},
|
|
absent: []string{"email", "email_verified"},
|
|
},
|
|
{
|
|
name: "email scope",
|
|
scope: "openid email",
|
|
status: http.StatusOK,
|
|
claims: map[string]interface{}{
|
|
"sub": "42",
|
|
"email": "john@example.tld",
|
|
"email_verified": true,
|
|
},
|
|
absent: []string{"name", "preferred_username", "locale"},
|
|
},
|
|
{
|
|
name: "no token",
|
|
noToken: true,
|
|
status: http.StatusUnauthorized,
|
|
},
|
|
{
|
|
name: "unknown user",
|
|
scope: "openid",
|
|
notFound: true,
|
|
status: http.StatusUnauthorized,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcc {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var (
|
|
rq = require.New(t)
|
|
r = httptest.NewRequest(http.MethodGet, "/oauth2/userinfo", nil)
|
|
rec = httptest.NewRecorder()
|
|
)
|
|
|
|
if !tc.noToken {
|
|
signed, err := auth.TokenIssuer.Sign(
|
|
auth.WithIdentity(auth.Authenticated(user.ID)),
|
|
auth.WithScope(strings.Split(tc.scope, " ")...),
|
|
)
|
|
rq.NoError(err)
|
|
|
|
token, err := jwt.Parse(signed)
|
|
rq.NoError(err)
|
|
|
|
r = r.WithContext(jwtauth.NewContext(r.Context(), token, nil))
|
|
}
|
|
|
|
h := &AuthHandlers{
|
|
Log: zap.NewNop(),
|
|
UserService: userServiceMocked{
|
|
findByAny: func(context.Context, interface{}) (*types.User, error) {
|
|
if tc.notFound {
|
|
return nil, fmt.Errorf("not found")
|
|
}
|
|
|
|
return user, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
h.oauth2Userinfo(rec, r)
|
|
rq.Equal(tc.status, rec.Code)
|
|
|
|
if tc.status != http.StatusOK {
|
|
rq.Contains(rec.Header().Get("WWW-Authenticate"), "Bearer")
|
|
return
|
|
}
|
|
|
|
var response map[string]interface{}
|
|
rq.NoError(json.Unmarshal(rec.Body.Bytes(), &response))
|
|
|
|
for claim, value := range tc.claims {
|
|
rq.Equal(value, response[claim], "claim %q", claim)
|
|
}
|
|
|
|
for _, claim := range tc.absent {
|
|
rq.NotContains(response, claim)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_SubSplitRoles(t *testing.T) {
|
|
type (
|
|
exp struct {
|
|
id string
|
|
i string
|
|
ii []string
|
|
}
|
|
)
|
|
var (
|
|
req = require.New(t)
|
|
ti = &oauth2models.Token{}
|
|
d = make(map[string]interface{})
|
|
|
|
tii = []exp{
|
|
{id: "1", i: "1", ii: []string{}},
|
|
{id: "1 2", i: "1", ii: []string{"2"}},
|
|
{id: "1 2 33 444", i: "1", ii: []string{"2", "33", "444"}},
|
|
}
|
|
)
|
|
|
|
for _, v := range tii {
|
|
ti.SetUserID(v.id)
|
|
SubSplit(ti, d)
|
|
|
|
req.Equal(v.i, d["sub"])
|
|
|
|
if _, is := d["roles"]; is {
|
|
req.Equal(v.ii, d["roles"])
|
|
}
|
|
}
|
|
}
|