2016-02-17 06:19:52 -06:00
package providers
import (
2019-08-06 13:20:54 +02:00
"context"
"fmt"
"strings"
"time"
2016-02-17 06:19:52 -06:00
2019-08-06 13:20:54 +02:00
oidc "github.com/coreos/go-oidc"
2020-09-30 01:44:42 +09:00
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
2019-08-06 13:20:54 +02:00
"golang.org/x/oauth2"
2016-02-17 06:19:52 -06:00
)
2019-08-06 13:20:54 +02:00
// GitLabProvider represents a GitLab based Identity Provider
2016-02-17 06:19:52 -06:00
type GitLabProvider struct {
* ProviderData
2019-08-06 13:20:54 +02:00
2020-06-27 01:26:07 +03:00
Groups [ ] string
2019-08-06 13:20:54 +02:00
EmailDomains [ ] string
Verifier * oidc . IDTokenVerifier
AllowUnverifiedEmail bool
2016-02-17 06:19:52 -06:00
}
2020-05-06 00:53:33 +09:00
var _ Provider = ( * GitLabProvider ) ( nil )
2020-05-25 13:08:04 +01:00
const (
gitlabProviderName = "GitLab"
gitlabDefaultScope = "openid email"
)
2018-12-20 10:37:59 +00:00
// NewGitLabProvider initiates a new GitLabProvider
2016-02-17 06:19:52 -06:00
func NewGitLabProvider ( p * ProviderData ) * GitLabProvider {
2020-05-25 13:08:04 +01:00
p . ProviderName = gitlabProviderName
2019-08-06 13:20:54 +02:00
if p . Scope == "" {
2020-05-25 13:08:04 +01:00
p . Scope = gitlabDefaultScope
2019-08-06 13:20:54 +02:00
}
return & GitLabProvider { ProviderData : p }
}
// Redeem exchanges the OAuth2 authentication token for an ID token
2020-05-06 00:53:33 +09:00
func ( p * GitLabProvider ) Redeem ( ctx context . Context , redirectURL , code string ) ( s * sessions . SessionState , err error ) {
2020-02-15 14:44:39 +01:00
clientSecret , err := p . GetClientSecret ( )
if err != nil {
return
}
2019-08-06 13:20:54 +02:00
c := oauth2 . Config {
ClientID : p . ClientID ,
2020-02-15 14:44:39 +01:00
ClientSecret : clientSecret ,
2019-08-06 13:20:54 +02:00
Endpoint : oauth2 . Endpoint {
TokenURL : p . RedeemURL . String ( ) ,
} ,
RedirectURL : redirectURL ,
}
token , err := c . Exchange ( ctx , code )
if err != nil {
return nil , fmt . Errorf ( "token exchange: %v" , err )
}
s , err = p . createSessionState ( ctx , token )
if err != nil {
return nil , fmt . Errorf ( "unable to update session: %v" , err )
}
return
}
// RefreshSessionIfNeeded checks if the session has expired and uses the
// RefreshToken to fetch a new ID token if required
2020-05-06 00:53:33 +09:00
func ( p * GitLabProvider ) RefreshSessionIfNeeded ( ctx context . Context , s * sessions . SessionState ) ( bool , error ) {
2020-05-30 08:53:38 +01:00
if s == nil || ( s . ExpiresOn != nil && s . ExpiresOn . After ( time . Now ( ) ) ) || s . RefreshToken == "" {
2019-08-06 13:20:54 +02:00
return false , nil
}
origExpiration := s . ExpiresOn
2020-05-06 00:53:33 +09:00
err := p . redeemRefreshToken ( ctx , s )
2019-08-06 13:20:54 +02:00
if err != nil {
return false , fmt . Errorf ( "unable to redeem refresh token: %v" , err )
}
fmt . Printf ( "refreshed id token %s (expired on %s)\n" , s , origExpiration )
return true , nil
}
2020-05-06 00:53:33 +09:00
func ( p * GitLabProvider ) redeemRefreshToken ( ctx context . Context , s * sessions . SessionState ) ( err error ) {
2020-02-15 14:44:39 +01:00
clientSecret , err := p . GetClientSecret ( )
if err != nil {
return
}
2019-08-06 13:20:54 +02:00
c := oauth2 . Config {
ClientID : p . ClientID ,
2020-02-15 14:44:39 +01:00
ClientSecret : clientSecret ,
2019-08-06 13:20:54 +02:00
Endpoint : oauth2 . Endpoint {
TokenURL : p . RedeemURL . String ( ) ,
} ,
}
t := & oauth2 . Token {
RefreshToken : s . RefreshToken ,
Expiry : time . Now ( ) . Add ( - time . Hour ) ,
}
token , err := c . TokenSource ( ctx , t ) . Token ( )
if err != nil {
return fmt . Errorf ( "failed to get token: %v" , err )
}
newSession , err := p . createSessionState ( ctx , token )
if err != nil {
return fmt . Errorf ( "unable to update session: %v" , err )
}
s . AccessToken = newSession . AccessToken
s . IDToken = newSession . IDToken
s . RefreshToken = newSession . RefreshToken
s . CreatedAt = newSession . CreatedAt
s . ExpiresOn = newSession . ExpiresOn
s . Email = newSession . Email
return
}
type gitlabUserInfo struct {
Username string ` json:"nickname" `
Email string ` json:"email" `
EmailVerified bool ` json:"email_verified" `
Groups [ ] string ` json:"groups" `
}
2020-05-06 00:53:33 +09:00
func ( p * GitLabProvider ) getUserInfo ( ctx context . Context , s * sessions . SessionState ) ( * gitlabUserInfo , error ) {
2019-08-06 13:20:54 +02:00
// Retrieve user info JSON
// https://docs.gitlab.com/ee/integration/openid_connect_provider.html#shared-information
// Build user info url from login url of GitLab instance
userInfoURL := * p . LoginURL
userInfoURL . Path = "/oauth/userinfo"
var userInfo gitlabUserInfo
2020-07-03 19:27:25 +01:00
err := requests . New ( userInfoURL . String ( ) ) .
WithContext ( ctx ) .
SetHeader ( "Authorization" , "Bearer " + s . AccessToken ) .
2020-07-06 17:42:26 +01:00
Do ( ) .
2020-07-03 19:27:25 +01:00
UnmarshalInto ( & userInfo )
2019-08-06 13:20:54 +02:00
if err != nil {
2020-07-03 19:27:25 +01:00
return nil , fmt . Errorf ( "error getting user info: %v" , err )
2019-08-06 13:20:54 +02:00
}
return & userInfo , nil
}
func ( p * GitLabProvider ) verifyGroupMembership ( userInfo * gitlabUserInfo ) error {
2020-06-27 01:26:07 +03:00
if len ( p . Groups ) == 0 {
2019-08-06 13:20:54 +02:00
return nil
}
// Collect user group memberships
membershipSet := make ( map [ string ] bool )
for _ , group := range userInfo . Groups {
membershipSet [ group ] = true
}
// Find a valid group that they are a member of
2020-06-27 01:26:07 +03:00
for _ , validGroup := range p . Groups {
2019-08-06 13:20:54 +02:00
if _ , ok := membershipSet [ validGroup ] ; ok {
return nil
2016-02-17 06:19:52 -06:00
}
}
2019-08-06 13:20:54 +02:00
2020-06-27 01:26:07 +03:00
return fmt . Errorf ( "user is not a member of '%s'" , p . Groups )
2019-08-06 13:20:54 +02:00
}
func ( p * GitLabProvider ) verifyEmailDomain ( userInfo * gitlabUserInfo ) error {
if len ( p . EmailDomains ) == 0 || p . EmailDomains [ 0 ] == "*" {
return nil
}
for _ , domain := range p . EmailDomains {
if strings . HasSuffix ( userInfo . Email , domain ) {
return nil
2016-02-17 06:19:52 -06:00
}
}
2019-08-06 13:20:54 +02:00
return fmt . Errorf ( "user email is not one of the valid domains '%v'" , p . EmailDomains )
}
func ( p * GitLabProvider ) createSessionState ( ctx context . Context , token * oauth2 . Token ) ( * sessions . SessionState , error ) {
rawIDToken , ok := token . Extra ( "id_token" ) . ( string )
if ! ok {
return nil , fmt . Errorf ( "token response did not contain an id_token" )
2016-02-17 06:19:52 -06:00
}
2019-08-06 13:20:54 +02:00
// Parse and verify ID Token payload.
idToken , err := p . Verifier . Verify ( ctx , rawIDToken )
if err != nil {
return nil , fmt . Errorf ( "could not verify id_token: %v" , err )
}
2020-05-30 08:53:38 +01:00
created := time . Now ( )
2019-08-06 13:20:54 +02:00
return & sessions . SessionState {
AccessToken : token . AccessToken ,
IDToken : rawIDToken ,
RefreshToken : token . RefreshToken ,
2020-05-30 08:53:38 +01:00
CreatedAt : & created ,
ExpiresOn : & idToken . Expiry ,
2019-08-06 13:20:54 +02:00
} , nil
}
// ValidateSessionState checks that the session's IDToken is still valid
2020-05-06 00:53:33 +09:00
func ( p * GitLabProvider ) ValidateSessionState ( ctx context . Context , s * sessions . SessionState ) bool {
2019-08-06 13:20:54 +02:00
_ , err := p . Verifier . Verify ( ctx , s . IDToken )
2020-04-14 17:36:44 +09:00
return err == nil
2016-02-17 06:19:52 -06:00
}
2018-12-20 10:37:59 +00:00
// GetEmailAddress returns the Account email address
2020-05-06 00:53:33 +09:00
func ( p * GitLabProvider ) GetEmailAddress ( ctx context . Context , s * sessions . SessionState ) ( string , error ) {
2019-08-06 13:20:54 +02:00
// Retrieve user info
2020-05-06 00:53:33 +09:00
userInfo , err := p . getUserInfo ( ctx , s )
2019-08-06 13:20:54 +02:00
if err != nil {
return "" , fmt . Errorf ( "failed to retrieve user info: %v" , err )
}
// Check if email is verified
if ! p . AllowUnverifiedEmail && ! userInfo . EmailVerified {
return "" , fmt . Errorf ( "user email is not verified" )
}
// Check if email has valid domain
err = p . verifyEmailDomain ( userInfo )
if err != nil {
return "" , fmt . Errorf ( "email domain check failed: %v" , err )
}
2016-02-17 06:19:52 -06:00
2019-08-06 13:20:54 +02:00
// Check group membership
err = p . verifyGroupMembership ( userInfo )
2016-02-17 06:19:52 -06:00
if err != nil {
2019-08-06 13:20:54 +02:00
return "" , fmt . Errorf ( "group membership check failed: %v" , err )
2016-02-17 06:19:52 -06:00
}
2019-08-06 13:20:54 +02:00
return userInfo . Email , nil
}
// GetUserName returns the Account user name
2020-05-06 00:53:33 +09:00
func ( p * GitLabProvider ) GetUserName ( ctx context . Context , s * sessions . SessionState ) ( string , error ) {
userInfo , err := p . getUserInfo ( ctx , s )
2016-02-17 06:19:52 -06:00
if err != nil {
2019-08-06 13:20:54 +02:00
return "" , fmt . Errorf ( "failed to retrieve user info: %v" , err )
2016-02-17 06:19:52 -06:00
}
2019-08-06 13:20:54 +02:00
return userInfo . Username , nil
2016-02-17 06:19:52 -06:00
}