Validate AWS Cognito JWT in GO - go

I am trying validate JWT returned from a login from AWS Cognito (hosted UI). I noticed that once the login is done in cognito, it tries to access my app with some params like "id_token" and "access_token". Checked with jwt.io and looks like "id_token" is the jwt.
As a test, I wrote a post function in GO expecting a body with the jwt token and the access token (and implemented from this answer)
func auth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
keyset, err := jwk.Fetch(context.Background(), "https://cognito-idp.{Region}.amazonaws.com/{poolID}/.well-known/jwks.json")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(&model.ErrorResponse{
Response: model.Response{
Result: false,
},
StatusCd: "500",
StatusDesc: "Failed to fetch jwks. Authorization failed.",
Error: "errRes",
})
}
authRequest := &model.AuthRequest{}
json.NewDecoder(r.Body).Decode(&authRequest)
parsedToken, err := jwt.Parse(
[]byte(authRequest.Token), //This is the JWT
jwt.WithKeySet(keyset),
jwt.WithValidate(true),
jwt.WithIssuer("https://cognito-idp.{Region}.amazonaws.com/{poolID}"),
jwt.WithAudience("{XX APP CLIENT ID XX}"),
jwt.WithClaimValue("key", authRequest.Access), //This is the Access Token
)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(&model.ErrorResponse{
Response: model.Response{
Result: false,
},
StatusCd: "500",
StatusDesc: "Failed token parse. Authorization failed.",
Error: "errRes",
})
}
result := parsedToken
json.NewEncoder(w).Encode(result)
}
Packages I am using are
"github.com/lestrrat-go/jwx/jwk"
"github.com/lestrrat-go/jwx/jwt"
Obviously, it failed at the token parse. What am I doing wrong and also what should I do with the parsedToken ?
I am new to this so, I have no clue if this is the correct approach and would really like some guidance.

If you're using the github.com/golang-jwt/jwt package (formally known as github.com/dgrijalva/jwt-go,) then you'd probably benefit from this example:
You can check out more JWKs Go examples here: github.com/MicahParks/keyfunc/tree/master/examples.
package main
import (
"fmt"
"log"
"time"
"github.com/golang-jwt/jwt"
"github.com/MicahParks/keyfunc"
)
func main() {
// Get the JWKs URL from your AWS region and userPoolId.
//
// See the AWS docs here:
// https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
regionID := "" // TODO Get the region ID for your AWS Cognito instance.
userPoolID := "" // TODO Get the user pool ID of your AWS Cognito instance.
jwksURL := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", regionID, userPoolID)
// Create the keyfunc options. Use an error handler that logs. Refresh the JWKs when a JWT signed by an unknown KID
// is found or at the specified interval. Rate limit these refreshes. Timeout the initial JWKs refresh request after
// 10 seconds. This timeout is also used to create the initial context.Context for keyfunc.Get.
refreshInterval := time.Hour
refreshRateLimit := time.Minute * 5
refreshTimeout := time.Second * 10
refreshUnknownKID := true
options := keyfunc.Options{
RefreshErrorHandler: func(err error) {
log.Printf("There was an error with the jwt.KeyFunc\nError:%s\n", err.Error())
},
RefreshInterval: &refreshInterval,
RefreshRateLimit: &refreshRateLimit,
RefreshTimeout: &refreshTimeout,
RefreshUnknownKID: &refreshUnknownKID,
}
// Create the JWKs from the resource at the given URL.
jwks, err := keyfunc.Get(jwksURL, options)
if err != nil {
log.Fatalf("Failed to create JWKs from resource at the given URL.\nError:%s\n", err.Error())
}
// Get a JWT to parse.
jwtB64 := "eyJraWQiOiJmNTVkOWE0ZSIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJLZXNoYSIsImF1ZCI6IlRhc2h1YW4iLCJpc3MiOiJqd2tzLXNlcnZpY2UuYXBwc3BvdC5jb20iLCJleHAiOjE2MTkwMjUyMTEsImlhdCI6MTYxOTAyNTE3NywianRpIjoiMWY3MTgwNzAtZTBiOC00OGNmLTlmMDItMGE1M2ZiZWNhYWQwIn0.vetsI8W0c4Z-bs2YCVcPb9HsBm1BrMhxTBSQto1koG_lV-2nHwksz8vMuk7J7Q1sMa7WUkXxgthqu9RGVgtGO2xor6Ub0WBhZfIlFeaRGd6ZZKiapb-ASNK7EyRIeX20htRf9MzFGwpWjtrS5NIGvn1a7_x9WcXU9hlnkXaAWBTUJ2H73UbjDdVtlKFZGWM5VGANY4VG7gSMaJqCIKMxRPn2jnYbvPIYz81sjjbd-sc2-ePRjso7Rk6s382YdOm-lDUDl2APE-gqkLWdOJcj68fc6EBIociradX_ADytj-JYEI6v0-zI-8jSckYIGTUF5wjamcDfF5qyKpjsmdrZJA"
// Parse the JWT.
token, err := jwt.Parse(jwtB64, jwks.KeyFunc)
if err != nil {
log.Fatalf("Failed to parse the JWT.\nError:%s\n", err.Error())
}
// Check if the token is valid.
if !token.Valid {
log.Fatalf("The token is not valid.")
}
log.Println("The token is valid.")
}

I would suggest to start out by doing the minimal checks -- i.e., first try just parsing without validation, then add validations one by one:
jwt.Parse([]byte(token)) // probably fails because of JWS
jwt.Parse([]byte(token), jwt.WithKeySet(...)) // should be OK?
jwt.Parse(..., jwt.WithValidation(true), ...) // add conditions one by one
Please note that I have no idea what's in id_token, as I have never used Cognito If it's a raw JWT, you shouldn't need a key set, and (1) should work.

Related

Getting Access token is empty error while running msgraph-sdk-go training example code

While trying to run the msgraph-sdk-go training code from here: https://github.com/microsoftgraph/msgraph-training-go, I'm getting InvalidAuthenticationTokenmsg: Access token is empty while executing the Graph API calls.
I configured a Microsoft developer account with instant sandbox for trial purpose.
I created an app registration as mentioned in the tutorial here and granted required permissions for the app.
The code is able to get the AppToken, but for calls to get Users, it fails with the above error. Am I missing something here?
I tried below code from the example for msgraph-training
func (g *GraphHelper) InitializeGraphForAppAuth() error {
clientId := os.Getenv("CLIENT_ID")
tenantId := os.Getenv("TENANT_ID")
clientSecret := os.Getenv("CLIENT_SECRET")
credential, err := azidentity.NewClientSecretCredential(tenantId, clientId, clientSecret, nil)
if err != nil {
return err
}
g.clientSecretCredential = credential
// Create an auth provider using the credential
authProvider, err := auth.NewAzureIdentityAuthenticationProviderWithScopes(g.clientSecretCredential, []string{
"https://graph.microsoft.com/.default",
})
if err != nil {
return err
}
// Create a request adapter using the auth provider
adapter, err := msgraphsdk.NewGraphRequestAdapter(authProvider)
if err != nil {
return err
}
// Create a Graph client using request adapter
client := msgraphsdk.NewGraphServiceClient(adapter)
g.appClient = client
return nil
}
// This part works, and I get the AppToken with required scope, once decoded.
func (g *GraphHelper) GetAppToken() (*string, error) {
token, err := g.clientSecretCredential.GetToken(context.Background(), policy.TokenRequestOptions{
Scopes: []string{
"https://graph.microsoft.com/.default",
},
})
if err != nil {
return nil, err
}
fmt.Println("expires on : ", token.ExpiresOn)
return &token.Token, nil
}
// The GetUsers function errors out
func (g *GraphHelper) GetUsers() (models.UserCollectionResponseable, error) {
var topValue int32 = 25
query := users.UsersRequestBuilderGetQueryParameters{
// Only request specific properties
Select: []string{"displayName", "id", "mail"},
// Get at most 25 results
Top: &topValue,
// Sort by display name
Orderby: []string{"displayName"},
}
resp, err := g.appClient.Users().
Get(context.Background(),
&users.UsersRequestBuilderGetRequestConfiguration{
QueryParameters: &query,
})
if err != nil {
fmt.Println("Users.Get got Error", err.Error(), resp)
printOdataError(err)
}
resp, err = g.appClient.Users().
Get(context.Background(),
nil)
if err != nil {
fmt.Println("Users.Get got Error with nil", err.Error(), resp)
}
return resp, err
}
I have added the User.Read.All permission in the app as mentioned in the tutorial.
Instead of getting the list of users, I'm getting below error:
Users.Get got Error error status code received from the API <nil>
error: error status code received from the API
code: InvalidAuthenticationTokenmsg: Access token is empty.Users.Get got Error with nil error status code received from the API <nil>
As you are using client Credential Flow ,you can verify your permission in azure portal , if you have User.Read.All delegated permission , Removes the delegated ones and add the corresponding application ones and don't forget to click on grant administrator consent after that. It should then work.
Hope this helps
Thanks.
Okay, so the fix that did work for me after trial and error was a version mismatch in the example and the actual application I was trying out.
The version of the beta msgraph application I was using was v0.49, whereas the msgraphsdk tutorial was using v0.48. The go mod command picked up the latest v0.49 initially I guess, I updated the go.mod file to use v0.48 after looking at the go.mod file from msgraph-training repository and things started working.
Hope this helps someone else later on.

How do I generate a token with TTL 8h from the below golang function?

I am new in programming and have no idea about using the the token generate client api function in the source code from my client side golang program. Looking for some advice. Thank you so much.
Source code package: https://pkg.go.dev/github.com/gravitational/teleport/api/client#Client.UpsertToken
Function Source Code:
func (c *Client) UpsertToken(ctx context.Context, token types.ProvisionToken) error {
tokenV2, ok := token.(*types.ProvisionTokenV2)
if !ok {
return trace.BadParameter("invalid type %T", token)
}
_, err := c.grpc.UpsertToken(ctx, tokenV2, c.callOpts...)
return trail.FromGRPC(err)
}
My code:
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/proto"
"google.golang.org/grpc"
)
// Client is a gRPC Client that connects to a Teleport Auth server either
// locally or over ssh through a Teleport web proxy or tunnel proxy.
//
// This client can be used to cover a variety of Teleport use cases,
// such as programmatically handling access requests, integrating
// with external tools, or dynamically configuring Teleport.
type Client struct {
// c contains configuration values for the client.
//c Config
// tlsConfig is the *tls.Config for a successfully connected client.
tlsConfig *tls.Config
// dialer is the ContextDialer for a successfully connected client.
//dialer ContextDialer
// conn is a grpc connection to the auth server.
conn *grpc.ClientConn
// grpc is the gRPC client specification for the auth server.
grpc proto.AuthServiceClient
// closedFlag is set to indicate that the connnection is closed.
// It's a pointer to allow the Client struct to be copied.
closedFlag *int32
// callOpts configure calls made by this client.
callOpts []grpc.CallOption
}
/*
type ProvisionToken interface {
Resource
// SetMetadata sets resource metatada
SetMetadata(meta Metadata)
// GetRoles returns a list of teleport roles
// that will be granted to the user of the token
// in the crendentials
GetRoles() SystemRoles
// SetRoles sets teleport roles
SetRoles(SystemRoles)
// GetAllowRules returns the list of allow rules
GetAllowRules() []*TokenRule
// GetAWSIIDTTL returns the TTL of EC2 IIDs
GetAWSIIDTTL() Duration
// V1 returns V1 version of the resource
V2() *ProvisionTokenSpecV2
// String returns user friendly representation of the resource
String() string
}
type ProvisionTokenSpecV2 struct {
// Roles is a list of roles associated with the token,
// that will be converted to metadata in the SSH and X509
// certificates issued to the user of the token
Roles []SystemRole `protobuf:"bytes,1,rep,name=Roles,proto3,casttype=SystemRole" json:"roles"`
Allow []*TokenRule `protobuf:"bytes,2,rep,name=allow,proto3" json:"allow,omitempty"`
AWSIIDTTL Duration `protobuf:"varint,3,opt,name=AWSIIDTTL,proto3,casttype=Duration" json:"aws_iid_ttl,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
*/
func main() {
ctx := context.Background()
args := os.Args[1:]
nodeType := ""
if len(args) > 0 {
nodeType = args[0]
}
proxyAddress := os.Getenv("TELEPORT_PROXY")
if len(proxyAddress) <= 0 {
proxyAddress = "proxy.teleport.example.local:443"
}
clt, err := client.New(ctx, client.Config{
Addrs: []string{
"proxy.teleport.example.local:443",
"proxy.teleport.example.local:3025",
"proxy.teleport.example.local:3024",
"proxy.teleport.example.local:3080",
},
Credentials: []client.Credentials{
client.LoadProfile("", ""),
},
})
if err != nil {
log.Fatalf("failed to create client: %v", err)
}
defer clt.Close()
ctx, err, token, err2 := clt.UpsertToken(ctx, token)
if err || err2 != nil {
log.Fatalf("failed to get tokens: %v", err)
}
now := time.Now()
t := 0
fmt.Printf("{\"tokens\": [")
for a, b := range token {
if strings.Contains(b.GetRoles(), b.Allow().String(), b.GetAWSIIDTTL(), nodeType) {
if t >= 1 {
fmt.Printf(",")
} else {
panic(err)
}
expiry := "never" //time.Now().Add(time.Hour * 8).Unix()
_ = expiry
if b.Expiry().Unix() > 0 {
exptime := b.Expiry().Format(time.RFC822)
expdur := b.Expiry().Sub(now).Round(time.Second)
expiry = fmt.Sprintf("%s (%s)", exptime, expdur.String())
}
fmt.Printf("\"count\": \"%1d\",", a)
fmt.Printf(b.Roles(), b.GetAllowRules(), b.GetAWSIIDTTL(), b.GetMetadata().Labels)
}
}
}
Output:
Syntax error instead of creating a token
It's seems your code have many mistake. And, It's very obvious you are getting syntax error. I am sure you would have got the line number in the console where actually these syntax error has occurred.
Please understand the syntax of Golang and also how to call the functions and how many parameter should i pass to those functions.
There are few mistakes i would like to point out after reviewing your code.
//It shouldn't be like this
ctx, err, token, err2 := clt.UpsertToken(ctx, token)
//Instead it should be like this
err := clt.UpsertToken(ctx, token)
//The return type of UpsertToken() method is error, you should use only one variable to receive this error.
strings.Contains() function takes two argument but you are passing four.
Refer this document for string.Contains()
You are assigning t := 0 and checking it with if condition inside for loop and never incremented.
Refer this document for fmt.Printf()
Refer this for function
Remove all the syntax error then only your code will run also cross check your logic.
If you want to see the example of syntax error then check here : https://go.dev/play/p/Hhu48UqlPRF

Parse/Validate JWT token from AzureAD in golang

I have Azure AD setup with OAuth2 and have it issuing a JWT for my web app. On subsequent requests, I want to validate the JWT that was issued. I'm using github.com/dgrijalva/jwt-go to do so however it always fails.
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte("bW8ZcMjBCnJZS-ibX5UQDNStvx4"), nil
})
if err != nil {
return nil, err
}
I'm picking at random the kid claim from the public keys listed by MS here https://login.microsoftonline.com/common/discovery/v2.0/keys so I'm lost as this isn't working.
Has anyone done this before or have any pointers?
The repository you are using is no longer maintained as pointed out by the README.
I've been using it's official replacement, https://github.com/golang-jwt/jwt, and I have never experienced any problem. You should try it.
Annoyingly it was a Azure AD config issue and out of the box it will generate a JWT token for MS Graph and the whole auth process succeeds but when you try to validate the token it fails for some reason. Once you have setup Azure AD correctly for your app with a correct scope it validates properly. I blogged about the specifics here - https://blog.jonathanchannon.com/2022-01-29-azuread-golang/
The asset located at https://login.microsoftonline.com/common/discovery/v2.0/keys is what's known as a JWKS, JSON Web Key Set. If all you want to do is authenticate tokens signed by this service, you can use something similar to the below code snippet. I've authored a package just for this use case: github.com/MicahParks/keyfunc
Under the hood, this package will read and parse the cryptographic keys found in the JWKS, then associate JWTs with those keys based on their key ID, kid. It also has some logic around automatically refreshing a remote JWKS resource.
package main
import (
"context"
"log"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/MicahParks/keyfunc"
)
func main() {
// Get the JWKS URL.
jwksURL := "https://login.microsoftonline.com/common/discovery/v2.0/keys"
// Create a context that, when cancelled, ends the JWKS background refresh goroutine.
ctx, cancel := context.WithCancel(context.Background())
// Create the keyfunc options. Use an error handler that logs. Refresh the JWKS when a JWT signed by an unknown KID
// is found or at the specified interval. Rate limit these refreshes. Timeout the initial JWKS refresh request after
// 10 seconds. This timeout is also used to create the initial context.Context for keyfunc.Get.
options := keyfunc.Options{
Ctx: ctx,
RefreshErrorHandler: func(err error) {
log.Printf("There was an error with the jwt.Keyfunc\nError: %s", err.Error())
},
RefreshInterval: time.Hour,
RefreshRateLimit: time.Minute * 5,
RefreshTimeout: time.Second * 10,
RefreshUnknownKID: true,
}
// Create the JWKS from the resource at the given URL.
jwks, err := keyfunc.Get(jwksURL, options)
if err != nil {
log.Fatalf("Failed to create JWKS from resource at the given URL.\nError: %s", err.Error())
}
// Get a JWT to parse.
//
// This wasn't signed by Azure AD.
jwtB64 := "eyJraWQiOiJlZThkNjI2ZCIsInR5cCI6IkpXVCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJXZWlkb25nIiwiYXVkIjoiVGFzaHVhbiIsImlzcyI6Imp3a3Mtc2VydmljZS5hcHBzcG90LmNvbSIsImlhdCI6MTYzMTM2OTk1NSwianRpIjoiNDY2M2E5MTAtZWU2MC00NzcwLTgxNjktY2I3NDdiMDljZjU0In0.LwD65d5h6U_2Xco81EClMa_1WIW4xXZl8o4b7WzY_7OgPD2tNlByxvGDzP7bKYA9Gj--1mi4Q4li4CAnKJkaHRYB17baC0H5P9lKMPuA6AnChTzLafY6yf-YadA7DmakCtIl7FNcFQQL2DXmh6gS9J6TluFoCIXj83MqETbDWpL28o3XAD_05UP8VLQzH2XzyqWKi97mOuvz-GsDp9mhBYQUgN3csNXt2v2l-bUPWe19SftNej0cxddyGu06tXUtaS6K0oe0TTbaqc3hmfEiu5G0J8U6ztTUMwXkBvaknE640NPgMQJqBaey0E4u0txYgyvMvvxfwtcOrDRYqYPBnA"
// Parse the JWT.
var token *jwt.Token
if token, err = jwt.Parse(jwtB64, jwks.Keyfunc); err != nil {
log.Fatalf("Failed to parse the JWT.\nError: %s", err.Error())
}
// Check if the token is valid.
if !token.Valid {
log.Fatalf("The token is not valid.")
}
log.Println("The token is valid.")
// End the background refresh goroutine when it's no longer needed.
cancel()
// This will be ineffectual because the line above this canceled the parent context.Context.
// This method call is idempotent similar to context.CancelFunc.
jwks.EndBackground()
}

How do I set the S3 Request Payer option when using the aws-sdk-v2?

I am trying to use the aws-sdk-go-v2 to retrieve some data from an S3 bucket. In order to do so I need to be able to set the Request Payer option, however, since I am new to using the SDK, I have no idea how to do so.
I've tried setting this as an env variable AWS_REQUEST_PAYER=requester, but scanning the source code for this golang SDK quickly, I couldn't find that it would be picked up by the SDK as an option.
Using the SDK as directed also fails with an Unauthorized response:
import (
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
type awsArchive struct {
bucket string
config config.Config
client *s3.Client
}
func (s *awsArchive) download(uri string, dest string) error {
downloader := manager.NewDownloader(s.client)
paginator := s3.NewListObjectsV2Paginator(s.client, &s3.ListObjectsV2Input{
Bucket: &s.bucket,
Prefix: &uri,
})
for paginator.HasMorePages() {
page, err := paginator.NextPage(context.TODO())
if err != nil {
return err
}
for _, obj := range page.Contents {
if err := downloadToFile(downloader, dest, s.bucket, aws.ToString(obj.Key)); err != nil {
return err
}
}
}
return nil
}
func downloadToFile(downloader *manager.Downloader, targetDirectory, bucket, key string) error {
// Create the directories in the path
file := filepath.Join(targetDirectory, key)
if err := os.MkdirAll(filepath.Dir(file), 0775); err != nil {
return err
}
// Set up the local file
fd, err := os.Create(file)
if err != nil {
return err
}
defer fd.Close()
// Download the file using the AWS SDK for Go
fmt.Printf("Downloading s3://%s/%s to %s...\n", bucket, key, file)
_, err = downloader.Download(context.TODO(), fd, &s3.GetObjectInput{Bucket: &bucket, Key: &key})
return err
}
Error: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: ..., HostID: ..., api error AccessDenied: Access Denied
Would anyone be able to provide me with an example of using the Golang SDK to get S3 files from a Requestor Pays enabled bucket please, i.e. the equivalent of:
aws s3 sync --request-payer requester source_bucket destination_folder
It seems you can use field named 'RequestPayer' in the GetObjectInput struct. Found it from pkg document.
From the link:
type GetObjectInput struct {
// The bucket name containing the object. When using this action with an access
...
Bucket *string
// Key of the object to get.
// This member is required.
Key *string
...
...
// Confirms that the requester knows that they will be charged for the request.
// Bucket owners need not specify this parameter in their requests. For information
// about downloading objects from requester pays buckets, see Downloading Objects
// in Requestor Pays Buckets
// (https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectsinRequesterPaysBuckets.html)
// in the Amazon S3 Developer Guide.
RequestPayer types.RequestPayer
You can refer to 'RequestPayer' definition here.

How to implement multitenancy with one Keycloak realm per tenant in go

Currently I am trying to implement multitenancy in an OAuth2 secured application by using one Keycloak realm for each tenant. I am creating a prototype in Go but am not really bound to the language and could switch to Node.js or Java if I needed to. I figure that my following question would hold true if I switched language though.
At first, implementing multitenancy seemed pretty straight forward to me:
For each tenant, create a realm with the needed client configuration for my backend application.
The backend receives a request with the URL tenant-1.my-app.com. Parse that URL to retrieve the tenant to be used for authentication.
Connect to the OAuth2 provider (Keycloak in this case) and verify the request token.
Following a guide, I use golang.org/x/oauth2 and github.com/coreos/go-oidc. This is how I setup the OAuth 2 connection for a single realm:
provider, err := oidc.NewProvider(context.Background(), "http://keycloak.docker.localhost/auth/realms/tenant-1")
if err != nil {
panic(err)
}
oauth2Config := oauth2.Config{
ClientID: "my-app",
ClientSecret: "my-app-secret",
RedirectURL: "http://tenant-1.my-app.com/auth-callback",
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
state := "somestate"
verifier := provider.Verifier(&oidc.Config{
ClientID: "my-app",
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tenant, err := getTenantFromRequest(r)
if err != nil {
log.Println(err)
w.WriteHeader(400)
return
}
log.Printf("Received request for tenant %s\n", tenant)
// Check if auth is present
rawAccessToken := r.Header.Get("Authorization")
if rawAccessToken == "" {
log.Println("No Auth present, redirecting to auth code url...")
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
return
}
// Check if auth is valid
parts := strings.Split(rawAccessToken, " ")
if len(parts) != 2 {
w.WriteHeader(400)
return
}
_, err = verifier.Verify(context.Background(), parts[1])
if err != nil {
log.Printf("Error during auth verification (%s), redirecting to auth code url...\n", err)
http.Redirect(w, r, oauth2Config.AuthCodeURL(state), http.StatusFound)
return
}
// Authentication okay
w.WriteHeader(200)
})
log.Printf("Starting server (%s)...\n", proxyConfig.Url)
log.Fatal(http.ListenAndServe(proxyConfig.Url, nil))
This works fine, but now comes the next step, adding multitenancy. IMO it seems like I would need to create one oidc.Provider for every tenant, because the realm endpoint (http://keycloak.docker.localhost/auth/realms/tenant-1) needs to be set on in the Provider struct.
I am unsure if this is the correct way to approach this situation. I guess I would add a cache for oidc.Provider instances to avoid creating instances on every request. But is creating one
Even though this hasn't been fully used in production, here is how I prototyped a solution that I will probably end up using:
I assume that there must be a single oidc.Provider for every keycloak realm.
Therefore there will also always be one oidc.IDTokenVerifier for every realm.
To manage these instances, I created this interface:
// A mechanism that manages oidc.Provider and IDTokenVerifierInterface instances
type OAuth2ManagerInterface interface {
GetOAuthProviderForKeycloakRealm(ctx context.Context, tenant string) (*oidc.Provider, error)
GetOpenIdConnectVerifierForProvider(provider *oidc.Provider) (IDTokenVerifierInterface, error)
}
// Interface created to describe the oidc.IDTokenVerifier struct.
// This was created because the oidc modules does not define its own interface
type IDTokenVerifierInterface interface {
Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error)
}
Then this is the struct that will implement the manager interface:
type OAuth2Manager struct {
ProviderUrl string
ClientId string
// Maps keycloak provider urls onto oidc.Provider instances
// Used internally to avoid creating a new provider on each request
providers map[string]*oidc.Provider
// Lock to be used when accessing OAuth2Manager.providers
providerLock sync.Mutex
// Maps oidc.Provider instances onto oidc.IDTokenVerifier instances
// Used internally to avoid creating a new verifier on each request
verifiers map[*oidc.Provider]*oidc.IDTokenVerifier
// Lock to be used when accessing OAuth2Manager.verifiers
verifierLock sync.Mutex
}
Along with the functions that actually implement the interface:
func (manager *OAuth2Manager) GetProviderUrlForRealm(realm string) string {
return fmt.Sprintf("%s/%s", manager.ProviderUrl, realm)
}
func (manager *OAuth2Manager) GetOAuthProviderForKeycloakRealm(ctx context.Context, tenant string) (*oidc.Provider, error) {
providerUrl := manager.GetProviderUrlForRealm(tenant)
// Check if already exists ...
manager.providerLock.Lock()
if provider, alreadyExists := manager.providers[providerUrl]; alreadyExists {
manager.providerLock.Unlock()
return provider, nil
}
// ... create new instance if not
provider, err := oidc.NewProvider(ctx, providerUrl)
if err != nil {
manager.providerLock.Unlock()
return nil, err
}
manager.providers[providerUrl] = provider
manager.providerLock.Unlock()
log.Printf("Created new provider for provider url %s\n", providerUrl)
return provider, nil
}
func (manager *OAuth2Manager) GetOpenIdConnectVerifierForProvider(provider *oidc.Provider) (IDTokenVerifierInterface, error) {
// Check if already exists ...
manager.verifierLock.Lock()
if verifier, alreadyExists := manager.verifiers[provider]; alreadyExists {
manager.verifierLock.Unlock()
return verifier, nil
}
// ... create new instance if not
oidcConfig := &oidc.Config{
ClientID: manager.ClientId,
}
verifier := provider.Verifier(oidcConfig)
manager.verifiers[provider] = verifier
manager.verifierLock.Unlock()
log.Printf("Created new verifier for OAuth endpoint %v\n", provider.Endpoint())
return verifier, nil
}
It is important to use sync.Mutex locks if the providers are accessed concurrently.

Resources