2015-03-30 15:30:27 -04:00
package providers
import (
2015-06-23 13:23:47 -04:00
"bytes"
2020-04-14 17:36:44 +09:00
"context"
2015-03-30 15:30:27 -04:00
"encoding/base64"
2015-05-20 23:23:48 -04:00
"encoding/json"
"errors"
2015-06-23 13:23:47 -04:00
"fmt"
2015-08-20 03:07:02 -07:00
"io"
2023-09-04 11:34:54 +02:00
"net/http"
2015-03-30 15:30:27 -04:00
"net/url"
2022-02-15 11:18:32 +00:00
"os"
2015-03-30 15:30:27 -04:00
"strings"
2015-06-23 07:23:39 -04:00
"time"
2015-08-20 03:07:02 -07:00
2023-10-24 21:03:16 +02:00
"cloud.google.com/go/compute/metadata"
2022-02-15 11:18:32 +00:00
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
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/logger"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
2023-09-04 11:34:54 +02:00
"golang.org/x/oauth2"
2015-08-20 03:07:02 -07:00
"golang.org/x/oauth2/google"
2019-03-05 15:07:10 +01:00
admin "google.golang.org/api/admin/directory/v1"
2017-03-28 15:58:18 +02:00
"google.golang.org/api/googleapi"
2023-10-24 21:03:16 +02:00
"google.golang.org/api/impersonate"
2020-04-14 17:36:44 +09:00
"google.golang.org/api/option"
2015-03-30 15:30:27 -04:00
)
2018-12-20 10:37:59 +00:00
// GoogleProvider represents an Google based Identity Provider
2015-03-30 15:30:27 -04:00
type GoogleProvider struct {
* ProviderData
2020-09-26 17:29:34 -07:00
2015-11-09 00:47:44 +01:00
RedeemRefreshURL * url . URL
2020-09-26 19:24:06 -07:00
2020-10-23 20:53:38 -07:00
// groupValidator is a function that determines if the user in the passed
2020-09-26 17:29:34 -07:00
// session is a member of any of the configured Google groups.
2020-09-26 19:24:06 -07:00
//
// This hits the Google API for each group, so it is called on Redeem &
// Refresh. `Authorize` uses the results of this saved in `session.Groups`
// Since it is called on every request.
2020-10-23 20:53:38 -07:00
groupValidator func ( * sessions . SessionState ) bool
2015-03-30 15:30:27 -04:00
}
2020-05-06 00:53:33 +09:00
var _ Provider = ( * GoogleProvider ) ( nil )
2019-05-07 11:44:19 +01:00
type claims struct {
Subject string ` json:"sub" `
Email string ` json:"email" `
EmailVerified bool ` json:"email_verified" `
}
2020-05-25 13:08:04 +01:00
const (
googleProviderName = "Google"
googleDefaultScope = "profile email"
)
var (
// Default Login URL for Google.
// Pre-parsed URL of https://accounts.google.com/o/oauth2/auth?access_type=offline.
googleDefaultLoginURL = & url . URL {
Scheme : "https" ,
Host : "accounts.google.com" ,
Path : "/o/oauth2/auth" ,
// to get a refresh token. see https://developers.google.com/identity/protocols/OAuth2WebServer#offline
RawQuery : "access_type=offline" ,
2015-03-30 15:30:27 -04:00
}
2020-05-25 13:08:04 +01:00
// Default Redeem URL for Google.
// Pre-parsed URL of https://www.googleapis.com/oauth2/v3/token.
googleDefaultRedeemURL = & url . URL {
Scheme : "https" ,
Host : "www.googleapis.com" ,
Path : "/oauth2/v3/token" ,
2015-05-08 17:13:35 -04:00
}
2020-05-25 13:08:04 +01:00
// Default Validation URL for Google.
// Pre-parsed URL of https://www.googleapis.com/oauth2/v1/tokeninfo.
googleDefaultValidateURL = & url . URL {
Scheme : "https" ,
Host : "www.googleapis.com" ,
Path : "/oauth2/v1/tokeninfo" ,
2015-03-30 15:30:27 -04:00
}
2020-05-25 13:08:04 +01:00
)
2015-08-20 03:07:02 -07:00
2020-05-25 13:08:04 +01:00
// NewGoogleProvider initiates a new GoogleProvider
2022-02-15 11:18:32 +00:00
func NewGoogleProvider ( p * ProviderData , opts options . GoogleOptions ) ( * GoogleProvider , error ) {
2020-05-25 13:08:04 +01:00
p . setProviderDefaults ( providerDefaults {
name : googleProviderName ,
loginURL : googleDefaultLoginURL ,
redeemURL : googleDefaultRedeemURL ,
profileURL : nil ,
validateURL : googleDefaultValidateURL ,
scope : googleDefaultScope ,
} )
2022-02-15 11:18:32 +00:00
provider := & GoogleProvider {
2015-08-20 03:07:02 -07:00
ProviderData : p ,
2020-10-23 20:53:38 -07:00
// Set a default groupValidator to just always return valid (true), it will
2015-08-20 03:07:02 -07:00
// be overwritten if we configured a Google group restriction.
2020-10-23 20:53:38 -07:00
groupValidator : func ( * sessions . SessionState ) bool {
2015-08-20 03:07:02 -07:00
return true
} ,
}
2022-02-15 11:18:32 +00:00
2023-09-04 11:34:54 +02:00
if opts . ServiceAccountJSON != "" || opts . UseApplicationDefaultCredentials {
2025-07-24 11:25:54 +05:30
provider . configureGroups ( opts )
2022-02-15 11:18:32 +00:00
}
return provider , nil
2015-03-30 15:30:27 -04:00
}
2025-07-24 11:25:54 +05:30
func ( p * GoogleProvider ) configureGroups ( opts options . GoogleOptions ) {
adminService := getAdminService ( opts )
// Backwards compatibility with `--google-group` option
if len ( opts . Groups ) > 0 {
p . setAllowedGroups ( opts . Groups )
p . groupValidator = p . setGroupRestriction ( opts . Groups , adminService )
return
}
p . groupValidator = p . populateAllGroups ( adminService )
}
2019-05-07 11:44:19 +01:00
func claimsFromIDToken ( idToken string ) ( * claims , error ) {
2015-05-20 23:23:48 -04:00
2015-03-30 15:30:27 -04:00
// id_token is a base64 encode ID token payload
// https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo
2015-06-23 07:23:39 -04:00
jwt := strings . Split ( idToken , "." )
2018-03-08 16:44:11 -08:00
jwtData := strings . TrimSuffix ( jwt [ 1 ] , "=" )
b , err := base64 . RawURLEncoding . DecodeString ( jwtData )
2015-03-30 15:30:27 -04:00
if err != nil {
2019-05-07 11:44:19 +01:00
return nil , err
2015-03-30 15:30:27 -04:00
}
2015-05-20 23:23:48 -04:00
2019-05-07 11:44:19 +01:00
c := & claims { }
err = json . Unmarshal ( b , c )
2015-03-30 15:30:27 -04:00
if err != nil {
2019-05-07 11:44:19 +01:00
return nil , err
2015-03-30 15:30:27 -04:00
}
2019-05-07 11:44:19 +01:00
if c . Email == "" {
return nil , errors . New ( "missing email" )
2015-05-20 23:23:48 -04:00
}
2019-05-07 11:44:19 +01:00
if ! c . EmailVerified {
return nil , fmt . Errorf ( "email %s not listed as verified" , c . Email )
2015-06-23 07:23:39 -04:00
}
2019-05-07 11:44:19 +01:00
return c , nil
2015-03-30 15:30:27 -04:00
}
2018-12-20 10:37:59 +00:00
// Redeem exchanges the OAuth2 authentication token for an ID token
2022-03-13 06:08:33 -04:00
func ( p * GoogleProvider ) Redeem ( ctx context . Context , redirectURL , code , codeVerifier string ) ( * sessions . SessionState , error ) {
2015-06-23 13:23:47 -04:00
if code == "" {
2020-10-23 19:35:15 -07:00
return nil , ErrMissingCode
2015-06-23 13:23:47 -04:00
}
2020-02-15 14:44:39 +01:00
clientSecret , err := p . GetClientSecret ( )
if err != nil {
2020-09-26 17:29:34 -07:00
return nil , err
2020-02-15 14:44:39 +01:00
}
2015-06-23 13:23:47 -04:00
params := url . Values { }
2015-11-09 00:47:44 +01:00
params . Add ( "redirect_uri" , redirectURL )
2015-06-23 13:23:47 -04:00
params . Add ( "client_id" , p . ClientID )
2020-02-15 14:44:39 +01:00
params . Add ( "client_secret" , clientSecret )
2015-06-23 13:23:47 -04:00
params . Add ( "code" , code )
params . Add ( "grant_type" , "authorization_code" )
2022-03-13 06:08:33 -04:00
if codeVerifier != "" {
params . Add ( "code_verifier" , codeVerifier )
}
2015-06-23 13:23:47 -04:00
var jsonResponse struct {
AccessToken string ` json:"access_token" `
RefreshToken string ` json:"refresh_token" `
2015-06-23 07:23:39 -04:00
ExpiresIn int64 ` json:"expires_in" `
2018-11-29 14:26:41 +00:00
IDToken string ` json:"id_token" `
2015-06-23 13:23:47 -04:00
}
2020-07-03 19:27:25 +01:00
err = requests . New ( p . RedeemURL . String ( ) ) .
WithContext ( ctx ) .
WithMethod ( "POST" ) .
WithBody ( bytes . NewBufferString ( params . Encode ( ) ) ) .
SetHeader ( "Content-Type" , "application/x-www-form-urlencoded" ) .
2020-07-06 17:42:26 +01:00
Do ( ) .
2020-07-03 19:27:25 +01:00
UnmarshalInto ( & jsonResponse )
2015-06-23 13:23:47 -04:00
if err != nil {
2020-07-03 19:27:25 +01:00
return nil , err
2015-06-23 13:23:47 -04:00
}
2020-07-03 19:27:25 +01:00
2019-05-07 11:44:19 +01:00
c , err := claimsFromIDToken ( jsonResponse . IDToken )
2015-06-23 07:23:39 -04:00
if err != nil {
2020-09-26 17:29:34 -07:00
return nil , err
2015-06-23 07:23:39 -04:00
}
2020-05-30 08:53:38 +01:00
2021-03-06 15:33:40 -08:00
ss := & sessions . SessionState {
2015-06-23 07:23:39 -04:00
AccessToken : jsonResponse . AccessToken ,
2018-01-27 10:14:19 +00:00
IDToken : jsonResponse . IDToken ,
2015-06-23 07:23:39 -04:00
RefreshToken : jsonResponse . RefreshToken ,
2019-05-07 11:44:19 +01:00
Email : c . Email ,
User : c . Subject ,
2021-03-06 15:33:40 -08:00
}
ss . CreatedAtNow ( )
ss . ExpiresIn ( time . Duration ( jsonResponse . ExpiresIn ) * time . Second )
return ss , nil
2020-10-23 20:53:38 -07:00
}
2020-11-29 14:12:48 -08:00
// EnrichSession checks the listed Google Groups configured and adds any
2020-10-23 20:53:38 -07:00
// that the user is a member of to session.Groups.
2021-03-06 15:33:40 -08:00
func ( p * GoogleProvider ) EnrichSession ( _ context . Context , s * sessions . SessionState ) error {
2020-11-29 14:12:48 -08:00
// TODO (@NickMeves) - Move to pure EnrichSession logic and stop
2020-11-08 14:01:50 -08:00
// reusing legacy `groupValidator`.
//
// This is called here to get the validator to do the `session.Groups`
// populating logic.
2020-10-23 20:53:38 -07:00
p . groupValidator ( s )
2020-09-26 17:29:34 -07:00
2020-10-23 20:53:38 -07:00
return nil
2020-09-26 17:29:34 -07:00
}
2015-08-20 03:07:02 -07:00
// SetGroupRestriction configures the GoogleProvider to restrict access to the
2025-07-24 11:25:54 +05:30
// specified group(s).
func ( p * GoogleProvider ) setGroupRestriction ( groups [ ] string , adminService * admin . Service ) func ( * sessions . SessionState ) bool {
return func ( s * sessions . SessionState ) bool {
2020-09-26 19:24:06 -07:00
// Reset our saved Groups in case membership changed
// This is used by `Authorize` on every request
2025-07-24 11:25:54 +05:30
s . Groups = make ( [ ] string , 0 , len ( groups ) )
for _ , group := range groups {
2020-09-26 19:24:06 -07:00
if userInGroup ( adminService , group , s . Email ) {
s . Groups = append ( s . Groups , group )
2020-09-26 17:29:34 -07:00
}
}
2020-09-26 19:24:06 -07:00
return len ( s . Groups ) > 0
2015-08-20 03:07:02 -07:00
}
}
2025-07-24 11:25:54 +05:30
// populateAllGroups configures the GoogleProvider to allow access with all
// groups and populate session with all groups of the user when no specific
// groups are configured.
func ( p * GoogleProvider ) populateAllGroups ( adminService * admin . Service ) func ( s * sessions . SessionState ) bool {
return func ( s * sessions . SessionState ) bool {
// Get all groups of the user
groups , err := getUserGroups ( adminService , s . Email )
if err != nil {
logger . Errorf ( "Failed to get user groups for %s: %v" , s . Email , err )
s . Groups = [ ] string { }
return true // Allow access even if we can't get groups
}
// Populate session with all user groups
s . Groups = groups
return true // Always allow access when no specific groups are configured
}
}
2025-07-21 15:06:17 +08:00
// https://developers.google.com/admin-sdk/directory/reference/rest/v1/members/hasMember#authorization-scopes
var possibleScopesList = [ ... ] string {
admin . AdminDirectoryGroupMemberReadonlyScope ,
admin . AdminDirectoryGroupReadonlyScope ,
admin . AdminDirectoryGroupMemberScope ,
admin . AdminDirectoryGroupScope ,
}
func getOauth2TokenSource ( ctx context . Context , opts options . GoogleOptions , scope string ) oauth2 . TokenSource {
2023-09-04 11:34:54 +02:00
if opts . UseApplicationDefaultCredentials {
2023-10-24 21:03:16 +02:00
ts , err := impersonate . CredentialsTokenSource ( ctx , impersonate . CredentialsConfig {
TargetPrincipal : getTargetPrincipal ( ctx , opts ) ,
2025-07-21 15:06:17 +08:00
Scopes : [ ] string { scope } ,
2023-10-24 21:03:16 +02:00
Subject : opts . AdminEmail ,
2023-09-04 11:34:54 +02:00
} )
if err != nil {
logger . Fatal ( "failed to fetch application default credentials: " , err )
}
2025-07-21 15:06:17 +08:00
return ts
}
2023-09-04 11:34:54 +02:00
2025-07-21 15:06:17 +08:00
credentialsReader , err := os . Open ( opts . ServiceAccountJSON )
if err != nil {
logger . Fatal ( "couldn't open Google credentials file: " , err )
}
data , err := io . ReadAll ( credentialsReader )
if err != nil {
logger . Fatal ( "can't read Google credentials file:" , err )
}
conf , err := google . JWTConfigFromJSON ( data , scope )
if err != nil {
logger . Fatal ( "can't load Google credentials file:" , err )
}
conf . Subject = opts . AdminEmail
return conf . TokenSource ( ctx )
}
2025-07-24 11:25:54 +05:30
// getAdminService retrieves an oauth token for the admin api of Google
// AdminEmail has to be an administrative email on the domain that is
// checked. CredentialsFile is the path to a json file containing a Google service
// account credentials.
2025-07-21 15:06:17 +08:00
func getAdminService ( opts options . GoogleOptions ) * admin . Service {
ctx := context . Background ( )
var client * http . Client
for _ , scope := range possibleScopesList {
ts := getOauth2TokenSource ( ctx , opts , scope )
_ , err := ts . Token ( )
if err == nil {
client = oauth2 . NewClient ( ctx , ts )
break
2023-09-04 11:34:54 +02:00
}
2025-07-21 15:06:17 +08:00
if retrieveErr , ok := err . ( * oauth2 . RetrieveError ) ; ok {
retrieveErrBody := map [ string ] interface { } { }
if err := json . Unmarshal ( retrieveErr . Body , & retrieveErrBody ) ; err != nil {
logger . Fatal ( "error unmarshalling retrieveErr body:" , err )
}
if retrieveErrBody [ "error" ] == "unauthorized_client" && retrieveErrBody [ "error_description" ] == "Client is unauthorized to retrieve access tokens using this method, or client not authorized for any of the scopes requested." {
continue
}
logger . Fatal ( "error retrieving token:" , err )
2023-09-04 11:34:54 +02:00
}
}
2025-07-21 15:06:17 +08:00
if client == nil {
logger . Fatal ( "error: google credentials do not have enough permissions to access admin API scope" )
}
2020-04-14 17:36:44 +09:00
adminService , err := admin . NewService ( ctx , option . WithHTTPClient ( client ) )
2015-08-20 03:07:02 -07:00
if err != nil {
2019-02-10 08:37:45 -08:00
logger . Fatal ( err )
2015-08-20 03:07:02 -07:00
}
return adminService
}
2023-10-24 21:03:16 +02:00
func getTargetPrincipal ( ctx context . Context , opts options . GoogleOptions ) ( targetPrincipal string ) {
targetPrincipal = opts . TargetPrincipal
if targetPrincipal != "" {
return targetPrincipal
}
logger . Print ( "INFO: no target principal set, trying to automatically determine one instead." )
credential , err := google . FindDefaultCredentials ( ctx )
if err != nil {
logger . Fatal ( "failed to fetch application default credentials: " , err )
}
content := map [ string ] interface { } { }
err = json . Unmarshal ( credential . JSON , & content )
switch {
case err != nil && ! metadata . OnGCE ( ) :
logger . Fatal ( "unable to unmarshal Application Default Credentials JSON" , err )
case content [ "client_email" ] != nil :
targetPrincipal = fmt . Sprintf ( "%v" , content [ "client_email" ] )
case metadata . OnGCE ( ) :
2024-09-23 16:10:28 +02:00
targetPrincipal , err = metadata . EmailWithContext ( ctx , "" )
2023-10-24 21:03:16 +02:00
if err != nil {
logger . Fatal ( "error while calling the GCE metadata server" , err )
}
default :
logger . Fatal ( "unable to determine Application Default Credentials TargetPrincipal, try overriding with --target-principal instead." )
}
return targetPrincipal
}
2025-07-24 11:25:54 +05:30
// getUserGroups retrieves all groups that a user is a member of using the Google Admin Directory API
func getUserGroups ( service * admin . Service , email string ) ( [ ] string , error ) {
var allGroups [ ] string
var pageToken string
for {
req := service . Groups . List ( ) . UserKey ( email ) . MaxResults ( 200 )
if pageToken != "" {
req = req . PageToken ( pageToken )
}
groupsResp , err := req . Do ( )
if err != nil {
return nil , fmt . Errorf ( "failed to list groups for user %s: %v" , email , err )
}
for _ , group := range groupsResp . Groups {
if group . Email != "" {
allGroups = append ( allGroups , group . Email )
}
}
// Check if there are more pages
if groupsResp . NextPageToken == "" {
break
}
pageToken = groupsResp . NextPageToken
}
return allGroups , nil
}
2015-08-20 03:07:02 -07:00
2020-09-26 17:29:34 -07:00
func userInGroup ( service * admin . Service , group string , email string ) bool {
// Use the HasMember API to checking for the user's presence in each group or nested subgroups
req := service . Members . HasMember ( group , email )
r , err := req . Do ( )
if err == nil {
return r . IsMember
}
gerr , ok := err . ( * googleapi . Error )
switch {
case ok && gerr . Code == 404 :
logger . Errorf ( "error checking membership in group %s: group does not exist" , group )
case ok && gerr . Code == 400 :
// It is possible for Members.HasMember to return false even if the email is a group member.
// One case that can cause this is if the user email is from a different domain than the group,
// e.g. "member@otherdomain.com" in the group "group@mydomain.com" will result in a 400 error
// from the HasMember API. In that case, attempt to query the member object directly from the group.
req := service . Members . Get ( group , email )
2019-08-06 02:38:24 -07:00
r , err := req . Do ( )
2015-08-20 03:07:02 -07:00
if err != nil {
2020-09-26 17:29:34 -07:00
logger . Errorf ( "error using get API to check member %s of google group %s: user not in the group" , email , group )
return false
2015-08-20 03:07:02 -07:00
}
2020-09-26 17:29:34 -07:00
// If the non-domain user is found within the group, still verify that they are "ACTIVE".
// Do not count the user as belonging to a group if they have another status ("ARCHIVED", "SUSPENDED", or "UNKNOWN").
if r . Status == "ACTIVE" {
2019-08-06 02:38:24 -07:00
return true
2015-08-20 03:07:02 -07:00
}
2020-09-26 17:29:34 -07:00
default :
logger . Errorf ( "error checking group membership: %v" , err )
2015-08-20 03:07:02 -07:00
}
2019-08-06 02:38:24 -07:00
return false
2015-08-20 03:07:02 -07:00
}
2021-03-06 15:33:13 -08:00
// RefreshSession uses the RefreshToken to fetch new Access and ID Tokens
func ( p * GoogleProvider ) RefreshSession ( ctx context . Context , s * sessions . SessionState ) ( bool , error ) {
if s == nil || s . RefreshToken == "" {
2015-06-23 07:23:39 -04:00
return false , nil
}
2021-03-06 15:48:31 -08:00
err := p . redeemRefreshToken ( ctx , s )
2015-06-23 07:23:39 -04:00
if err != nil {
return false , err
}
2015-08-20 03:07:02 -07:00
2020-11-08 14:01:50 -08:00
// TODO (@NickMeves) - Align Group authorization needs with other providers'
// behavior in the `RefreshSession` case.
//
2015-08-20 03:07:02 -07:00
// re-check that the user is in the proper google group(s)
2020-10-23 20:53:38 -07:00
if ! p . groupValidator ( s ) {
2015-08-20 03:07:02 -07:00
return false , fmt . Errorf ( "%s is no longer in the group(s)" , s . Email )
}
2015-06-23 07:23:39 -04:00
return true , nil
}
2021-03-06 15:48:31 -08:00
func ( p * GoogleProvider ) redeemRefreshToken ( ctx context . Context , s * sessions . SessionState ) error {
2015-06-23 13:23:47 -04:00
// https://developers.google.com/identity/protocols/OAuth2WebServer#refresh
2020-02-15 14:44:39 +01:00
clientSecret , err := p . GetClientSecret ( )
if err != nil {
2021-03-06 15:48:31 -08:00
return err
2020-02-15 14:44:39 +01:00
}
2015-06-23 13:23:47 -04:00
params := url . Values { }
params . Add ( "client_id" , p . ClientID )
2020-02-15 14:44:39 +01:00
params . Add ( "client_secret" , clientSecret )
2021-03-06 15:48:31 -08:00
params . Add ( "refresh_token" , s . RefreshToken )
2015-06-23 13:23:47 -04:00
params . Add ( "grant_type" , "refresh_token" )
2015-06-23 07:23:39 -04:00
var data struct {
2015-06-23 13:23:47 -04:00
AccessToken string ` json:"access_token" `
2015-06-23 07:23:39 -04:00
ExpiresIn int64 ` json:"expires_in" `
2019-03-05 15:07:10 +01:00
IDToken string ` json:"id_token" `
2015-06-23 13:23:47 -04:00
}
2020-07-03 19:27:25 +01:00
err = requests . New ( p . RedeemURL . String ( ) ) .
WithContext ( ctx ) .
WithMethod ( "POST" ) .
WithBody ( bytes . NewBufferString ( params . Encode ( ) ) ) .
SetHeader ( "Content-Type" , "application/x-www-form-urlencoded" ) .
2020-07-06 17:42:26 +01:00
Do ( ) .
2020-07-03 19:27:25 +01:00
UnmarshalInto ( & data )
2015-06-23 13:23:47 -04:00
if err != nil {
2021-03-06 15:48:31 -08:00
return err
2015-06-23 13:23:47 -04:00
}
2020-07-03 19:27:25 +01:00
2021-03-06 15:48:31 -08:00
s . AccessToken = data . AccessToken
s . IDToken = data . IDToken
s . CreatedAtNow ( )
s . ExpiresIn ( time . Duration ( data . ExpiresIn ) * time . Second )
return nil
2015-06-23 13:23:47 -04:00
}