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()
}
Related
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.
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
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.
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.
Here is my golang app listening requests from slack command:
main.go:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
type SlackCmdResponse struct {
Text string `json:"text"`
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
if err := req.ParseForm(); err != nil {
panic(err)
}
responseUrl := req.PostFormValue("response_url")
go func() {
time.Sleep(time.Second)
postBack(responseUrl)
}()
rj, err := json.Marshal(SlackCmdResponse{Text: "Test started"})
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(rj)
})
fmt.Println("listening 8383")
if err := http.ListenAndServe(":8383", nil); err != nil {
panic(err)
}
}
func postBack(responseUrl string) {
fmt.Println("responseUrl", responseUrl)
cResp := SlackCmdResponse{
Text: "Test finished",
}
cj, err := json.Marshal(cResp)
if err != nil {
panic(err)
}
req, err := http.NewRequest("POST", responseUrl, bytes.NewReader(cj))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
if resp != nil {
fmt.Println(resp.StatusCode)
if b, err := ioutil.ReadAll(resp.Body); err != nil {
panic(err)
} else {
fmt.Println(string(b))
}
if resp.Body != nil {
resp.Body.Close()
}
}
}
I run it:
$ go run main.go
listening 8383
I use ngrok to make it accessible from the internet:
ngrok http 8383
I created slack slash command /my-command with POST option and pasted https URL that ngrok gave:
Now when I run /my-command in slack, I get slack reply Test started. And then in a second slack prints Test finished
For now, it's all fine.
Problem
If I replace line
time.Sleep(time.Second)
by line
time.Sleep(time.Hour) // long test
I don't get slack printed "Test finished" in hour. Instead, I see in my app logs:
responseUrl https://hooks.slack.com/commands/T3GBFUZ64/86817661808/XRmDO21jYaP1Wzu7GFpNw9eW
404
Expired url
Looks like slack's response URL has an expiration time. How to extend this expiration time?
Or in a case of expiration, is there another way to send a message to the user about having the test finished? I have name and id of a user launching /my-command
req.PostFormValue("user_name")
req.PostFormValue("user_id")
So I want to run integration tests by slack which are longer than 2 hours and get a response after finishing such tests in slack.
You can not increase the expire time for the URL that is a Slack internal setting.
Using the Slack Web API
You can send unattended messages to any user through the Slack Web API which you need a token for it.
For more information check: https://api.slack.com/web.
chat.postMessage
The Slack API has a postMessage command that allows the user to post messages to a channel, to the slackbot channel and to the user through IM channel (direct message). It seems you want to do the later which is quite simple.
Post to an IM channel
chat.postMessage#channels
Method Url: https://slack.com/api/chat.postMessage
Posting to an IM channel is a little more complex when settings the value of channel depending on the value of as_user.
If as_user is false:
Pass a username (#chris) as the value of channel to post to that user's #slackbot channel as the bot.
Pass the IM channel's ID (D023BB3L2) as the value of channel to post to that IM channel as the bot. The IM channel's ID can be retrieved through the im.list API method.
If as_user is true:
Pass the IM channel's ID (D023BB3L2) as the value of channel to post to that IM channel as the authenticated user. The IM channel's ID can be retrieved through the im.list API method.
To send a direct message to the user owning the token used in the request, provide the channel field with the a conversation/IM ID value found in a method like im.list.
To send a direct message to the user owning the token used in the request, provide the channel field with the a conversation/IM ID value found in a method like im.list.
im.list
This method will return a list of direct messages channels if you don't have a channel open with the user yet you can call the im.open.
im.open
This method is used to open a direct message channel with a specified user.
Documentation about im.open can be found here.
Example URL
https://slack.com/api/chat.postMessage?token=**TOKEN**&channel=**Direct Channel ID**&text=HelloWorld&as_user=true&pretty=1
Just replace **TOKEN** and **Direct Channel ID** with your values and it should send a direct message to the specified user.