Merge pull request #7257 from reasonerjt/oidc-controllers-p2

Add callback controller for OIDC
This commit is contained in:
Wenkai Yin 2019-04-02 13:37:00 +08:00 committed by GitHub
commit 8d6299fed9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 143 additions and 30 deletions

View File

@ -135,5 +135,5 @@ const (
CoreConfigPath = "/api/internal/configurations"
RobotTokenDuration = "robot_token_duration"
OIDCCallbackPath = "/c/oidc_callback"
OIDCCallbackPath = "/c/oidc/callback"
)

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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{})