mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-23 10:45:45 +01:00
Add callback controller for OIDC
This commit add callback controller to handle the redirection from successful OIDC authentication. For E2E case this requires callback controller to kick off onboard process, which will be covered in subsequent commits. Signed-off-by: Daniel Jiang <jiangd@vmware.com>
This commit is contained in:
parent
4b82b0e364
commit
587acd33ad
@ -135,5 +135,5 @@ const (
|
||||
CoreConfigPath = "/api/internal/configurations"
|
||||
RobotTokenDuration = "robot_token_duration"
|
||||
|
||||
OIDCCallbackPath = "/c/oidc_callback"
|
||||
OIDCCallbackPath = "/c/oidc/callback"
|
||||
)
|
||||
|
@ -16,12 +16,15 @@ package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
gooidc "github.com/coreos/go-oidc"
|
||||
"github.com/goharbor/harbor/src/common/models"
|
||||
"github.com/goharbor/harbor/src/common/utils/log"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"golang.org/x/oauth2"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@ -32,14 +35,20 @@ const googleEndpoint = "https://accounts.google.com"
|
||||
|
||||
type providerHelper struct {
|
||||
sync.Mutex
|
||||
ep atomic.Value
|
||||
ep endpoint
|
||||
instance atomic.Value
|
||||
setting atomic.Value
|
||||
}
|
||||
|
||||
type endpoint struct {
|
||||
url string
|
||||
skipCertVerify bool
|
||||
}
|
||||
|
||||
func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
if p.instance.Load() != nil {
|
||||
if p.ep.Load().(string) != p.setting.Load().(models.OIDCSetting).Endpoint {
|
||||
s := p.setting.Load().(models.OIDCSetting)
|
||||
if s.Endpoint != p.ep.url || s.SkipCertVerify != p.ep.skipCertVerify { // relevant settings have changed, need to re-create provider.
|
||||
if err := p.create(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -48,7 +57,7 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
if p.instance.Load() == nil {
|
||||
if err := p.loadConf(); err != nil {
|
||||
if err := p.reload(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := p.create(); err != nil {
|
||||
@ -56,43 +65,67 @@ func (p *providerHelper) get() (*gooidc.Provider, error) {
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
if err := p.loadConf(); err != nil {
|
||||
log.Warningf(err.Error())
|
||||
if err := p.reload(); err != nil {
|
||||
log.Warningf("Failed to refresh configuration, error: %v", err)
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return p.instance.Load().(*gooidc.Provider), nil
|
||||
|
||||
}
|
||||
|
||||
func (p *providerHelper) loadConf() error {
|
||||
var c *models.OIDCSetting
|
||||
c, err := config.OIDCSetting()
|
||||
func (p *providerHelper) reload() error {
|
||||
conf, err := config.OIDCSetting()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load OIDC setting: %v", err)
|
||||
}
|
||||
p.setting.Store(*c)
|
||||
p.setting.Store(*conf)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *providerHelper) create() error {
|
||||
bc := context.Background()
|
||||
s := p.setting.Load().(models.OIDCSetting)
|
||||
provider, err := gooidc.NewProvider(bc, s.Endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
if p.setting.Load() == nil {
|
||||
return errors.New("the configuration is not loaded")
|
||||
}
|
||||
s := p.setting.Load().(models.OIDCSetting)
|
||||
var client *http.Client
|
||||
if s.SkipCertVerify {
|
||||
client = &http.Client{
|
||||
Transport: insecureTransport,
|
||||
}
|
||||
} else {
|
||||
client = &http.Client{}
|
||||
}
|
||||
ctx := context.Background()
|
||||
gooidc.ClientContext(ctx, client)
|
||||
provider, err := gooidc.NewProvider(ctx, s.Endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create OIDC provider, error: %v", err)
|
||||
}
|
||||
p.ep.Store(s.Endpoint)
|
||||
p.instance.Store(provider)
|
||||
p.ep = endpoint{
|
||||
url: s.Endpoint,
|
||||
skipCertVerify: s.SkipCertVerify,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var provider = &providerHelper{}
|
||||
|
||||
var insecureTransport = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Token wraps the attributes of a oauth2 token plus the attribute of ID token
|
||||
type Token struct {
|
||||
*oauth2.Token
|
||||
IDToken string `json:"id_token"`
|
||||
}
|
||||
|
||||
func getOauthConf() (*oauth2.Config, error) {
|
||||
p, err := provider.get()
|
||||
if err != nil {
|
||||
@ -129,3 +162,27 @@ func AuthCodeURL(state string) (string, error) {
|
||||
}
|
||||
return conf.AuthCodeURL(state), nil
|
||||
}
|
||||
|
||||
// ExchangeToken get the token from token provider via the code
|
||||
func ExchangeToken(ctx context.Context, code string) (*Token, error) {
|
||||
oauth, err := getOauthConf()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get OAuth configuration, error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
oauthToken, err := oauth.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Token{Token: oauthToken, IDToken: oauthToken.Extra("id_token").(string)}, nil
|
||||
}
|
||||
|
||||
// VerifyToken verifies the ID token based on the OIDC settings
|
||||
func VerifyToken(ctx context.Context, rawIDToken string) (*gooidc.IDToken, error) {
|
||||
p, err := provider.get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verifier := p.Verifier(&gooidc.Config{ClientID: provider.setting.Load().(models.OIDCSetting).ClientID})
|
||||
return verifier.Verify(ctx, rawIDToken)
|
||||
}
|
||||
|
@ -44,24 +44,23 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperLoadConf(t *testing.T) {
|
||||
testP := &providerHelper{}
|
||||
assert.Nil(t, testP.setting.Load())
|
||||
err := testP.loadConf()
|
||||
err := testP.reload()
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test", testP.setting.Load().(models.OIDCSetting).Name)
|
||||
assert.Nil(t, testP.ep.Load())
|
||||
assert.Equal(t, endpoint{}, testP.ep)
|
||||
}
|
||||
|
||||
func TestHelperCreate(t *testing.T) {
|
||||
testP := &providerHelper{}
|
||||
err := testP.loadConf()
|
||||
err := testP.reload()
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, testP.instance.Load())
|
||||
err = testP.create()
|
||||
assert.Nil(t, err)
|
||||
assert.EqualValues(t, "https://accounts.google.com", testP.ep.Load().(string))
|
||||
assert.EqualValues(t, "https://accounts.google.com", testP.ep.url)
|
||||
assert.NotNil(t, testP.instance.Load())
|
||||
}
|
||||
|
||||
|
@ -260,6 +260,6 @@ func TestOIDCSetting(t *testing.T) {
|
||||
assert.True(t, v.SkipCertVerify)
|
||||
assert.Equal(t, "client", v.ClientID)
|
||||
assert.Equal(t, "secret", v.ClientSecret)
|
||||
assert.Equal(t, "https://harbor.test/c/oidc_callback", v.RedirectURL)
|
||||
assert.Equal(t, "https://harbor.test/c/oidc/callback", v.RedirectURL)
|
||||
assert.ElementsMatch(t, []string{"openid", "profile"}, v.Scope)
|
||||
}
|
||||
|
@ -24,21 +24,75 @@ import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const idTokenKey = "oidc_id_token"
|
||||
const stateKey = "oidc_state"
|
||||
|
||||
// OIDCController handles requests for OIDC login, callback and user onboard
|
||||
type OIDCController struct {
|
||||
api.BaseController
|
||||
}
|
||||
|
||||
type oidcUserData struct {
|
||||
Issuer string `json:"iss"`
|
||||
Subject string `json:"sub"`
|
||||
Username string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// Prepare include public code path for call request handler of OIDCController
|
||||
func (oc *OIDCController) Prepare() {
|
||||
if mode, _ := config.AuthMode(); mode != common.OIDCAuth {
|
||||
oc.CustomAbort(http.StatusPreconditionFailed, fmt.Sprintf("Auth Mode: %s is not OIDC based.", mode))
|
||||
}
|
||||
}
|
||||
|
||||
// RedirectLogin redirect user's browser to OIDC provider's login page
|
||||
func (oc *OIDCController) RedirectLogin() {
|
||||
if mode, _ := config.AuthMode(); mode != common.OIDCAuth {
|
||||
oc.RenderError(http.StatusPreconditionFailed, fmt.Sprintf("Auth Mode: %s is not OIDC based.", mode))
|
||||
return
|
||||
}
|
||||
url, err := oidc.AuthCodeURL(utils.GenerateRandomString())
|
||||
state := utils.GenerateRandomString()
|
||||
url, err := oidc.AuthCodeURL(state)
|
||||
if err != nil {
|
||||
oc.RenderFormatedError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
oc.SetSession(stateKey, state)
|
||||
// Force to use the func 'Redirect' of beego.Controller
|
||||
oc.Controller.Redirect(url, http.StatusFound)
|
||||
}
|
||||
|
||||
// Callback handles redirection from OIDC provider. It will exchange the token and
|
||||
// kick off onboard if needed.
|
||||
func (oc *OIDCController) Callback() {
|
||||
if oc.Ctx.Request.URL.Query().Get("state") != oc.GetSession(stateKey) {
|
||||
oc.RenderError(http.StatusBadRequest, "State mismatch.")
|
||||
return
|
||||
}
|
||||
code := oc.Ctx.Request.URL.Query().Get("code")
|
||||
ctx := oc.Ctx.Request.Context()
|
||||
token, err := oidc.ExchangeToken(ctx, code)
|
||||
if err != nil {
|
||||
oc.RenderFormatedError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
idToken, err := oidc.VerifyToken(ctx, token.IDToken)
|
||||
if err != nil {
|
||||
oc.RenderFormatedError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
d := &oidcUserData{}
|
||||
err = idToken.Claims(d)
|
||||
if err != nil {
|
||||
oc.RenderFormatedError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
oc.SetSession(idTokenKey, token.IDToken)
|
||||
// TODO: check and trigger onboard popup or redirect user to project page
|
||||
oc.Data["json"] = d
|
||||
oc.ServeFormatted()
|
||||
}
|
||||
|
||||
// Onboard handles the request to onboard an user authenticated via OIDC provider
|
||||
func (oc *OIDCController) Onboard() {
|
||||
oc.RenderError(http.StatusNotImplemented, "")
|
||||
return
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/goharbor/harbor/src/common"
|
||||
"github.com/goharbor/harbor/src/core/api"
|
||||
"github.com/goharbor/harbor/src/core/config"
|
||||
"github.com/goharbor/harbor/src/core/controllers"
|
||||
@ -37,7 +38,9 @@ func initRouters() {
|
||||
beego.Router("/c/reset", &controllers.CommonController{}, "post:ResetPassword")
|
||||
beego.Router("/c/userExists", &controllers.CommonController{}, "post:UserExists")
|
||||
beego.Router("/c/sendEmail", &controllers.CommonController{}, "get:SendResetEmail")
|
||||
beego.Router("/c/oidc_login", &controllers.OIDCController{}, "get:RedirectLogin")
|
||||
beego.Router("/c/oidc/login", &controllers.OIDCController{}, "get:RedirectLogin")
|
||||
beego.Router("/c/oidc/onboard", &controllers.OIDCController{}, "post:Onboard")
|
||||
beego.Router(common.OIDCCallbackPath, &controllers.OIDCController{}, "get:Callback")
|
||||
|
||||
// API:
|
||||
beego.Router("/api/projects/:pid([0-9]+)/members/?:pmid([0-9]+)", &api.ProjectMemberAPI{})
|
||||
|
Loading…
Reference in New Issue
Block a user