Possible to specify a proxy when refreshing an OAuth 2.0 Authorization Code flow token? - go

I have an application that is using the OAuth 2.0 Authorization Code flow and as part of that we regularly check whether a token has expired and refresh if it has. This is all done using golang's built in oauth2 library. The code that performs the refresh looks something like this:
if time.Now().Before(currentToken.Expiry) {
return false, nil
}
// if the expiry has passed, generate a new token
refreshConf := oauth2.Config{
ClientID: config.OAuthConfig.ClientID,
ClientSecret: config.OAuthConfig.ClientSecret,
Endpoint: oauth2.Endpoint{
TokenURL: config.TokenURL,
},
}
tokenRefreshSource := refreshConf.TokenSource(ctx, &currentToken.Token)
newToken, err := tokenRefreshSource.Token()
if err != nil {
return false, err
}
currentToken.Token = *newToken
Recently my work has cracked down on unregulated outbound traffic in an effort to make things more secure. Part of these changes has required that all outbound traffic go through a specific proxy. I've been able to do this by setting http.Client.Transport.Proxy for the requests that I'm manually sending, but I have not found a way to have the underlying request that is fired from tokenRefreshSource.Token() use a proxy.
Does anyone know if this is possible? If so - suggestions on how I can do it?

Related

OAuth Authorization in Golang CLI Applications

I've been using the code from the OAuth2 package documentation to add authorization to my CLI application, but I've run into a bit of a snag.
package main
import (
"context"
"fmt"
"log"
"golang.org/x/oauth2"
)
func main() {
ctx := context.Background()
conf := &oauth2.Config{
ClientID: "YOUR_CLIENT_ID",
ClientSecret: "YOUR_CLIENT_SECRET",
Scopes: []string{"SCOPE1", "SCOPE2"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/o/oauth2/auth",
TokenURL: "https://provider.com/o/oauth2/token",
},
}
// Redirect user to consent page to ask for permission
// for the scopes specified above.
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
fmt.Printf("Visit the URL for the auth dialog: %v", url)
// Use the authorization code that is pushed to the redirect
// URL. Exchange will do the handshake to retrieve the
// initial access token. The HTTP Client returned by
// conf.Client will refresh the token as necessary.
var code string
if _, err := fmt.Scan(&code); err != nil {
log.Fatal(err)
}
tok, err := conf.Exchange(ctx, code)
if err != nil {
log.Fatal(err)
}
client := conf.Client(ctx, tok)
client.Get("...")
}
I'm using StackOverflow as the authorization server and api.stackexchange.com doc says the following. But I'm not sure what it means.
Desktop applications cannot participate directly in OAuth 2.0 flows, however the embeddable browser controls available in most frameworks make it possible to work around this limitation.
The issue is that when I use the method described in the documentation, I have to manually click the URL generated in the CLI and manually copy the access token to continue.
Do you happen to know of a way to automate these two steps in Golang or a different approach I could take for authorization in CLI applications?
What I'm hoping for is something like what you see in the heroku-cli but with OAuth 2.0 - it would be awesome if my app could handle authorization in a similar way. Thanks for your help!
I'd really appreciate any help or advice you can offer. Thanks!

Golang Oauth2 Service account returns empty refresh token string

I'm having a problem with the google/Oauth2 package when attempting to authenticate through the service account using a server to server authentication. Google responds with a token struct with an empty refresh token string, and the token expires in 1h, which I can't refresh as I don't have a refresh token.
Here is the code snippet I'm using:
/*
import(
"github.com/google/go-containerregistry/pkg/authn"
gcr "github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
)
*/
data, err := ioutil.ReadFile(fmt.Sprintf("%s/%s", path, serviceAccountFilePath))
if err != nil {
log.Fatalf("Failed to read GCP service account key file: %s", err)
}
ctx := context.Background()
fmt.Println(scopes)
creds, err := google.CredentialsFromJSON(ctx, data, scopes...)
if err != nil {
log.Fatalf("Failed to load GCP service account credentials: %s", err)
}
t, _ := creds.TokenSource.Token()
fmt.Println(t.Expiry.Sub(time.Now()).String(), t.RefreshToken, ">>>")
r, err := gcr.NewRegistry("https://gcr.io")
if err != nil {
log.Fatalf("failed to ping registry: %s", err)
}
authToken := authn.FromConfig(authn.AuthConfig{
RegistryToken: t.AccessToken,
})
repo, err := gcr.NewRepository(fmt.Sprintf("%s/%s", urlPrefix, imageName))
repo.Registry = r
list, err := remote.List(repo, remote.WithAuth(authToken))
I tried different ways while using the service account for authentication, such as the config and JWT but I still got the same result.
Service accounts don't need / use refresh tokens.
Refresh tokens are used for offline access by standard Oauth2 authorization. If the user is offline then the application can use the refresh tokens to get an new access token and make requests on behalf of the user.
With service accounts they are already preauthorized and have access to the data they have. A request should return an access token once that access token expires after an hour you just make a new authorization request to get a new access token.
Refresh tokens are unnecessary in the case of service accounts. When the access token expires just run your auth code again to get a new one. Its saving you a step.
Thanks to #DalmTo's hints, I solved the problem.
So the fix for the such problem was by not using the credentials out of google.CredentialsFromJSON() func will return the token source without refreshing the token in case of passing the service account to that function, which means that you can't refresh your token when it expires again later. Also, anticipating and re-authenticating to generate a new token didn't work for me (no clue why).
So I had to convert the JSON of the service account into JWT through this func instead
scopes := []string{"https://www.googleapis.com/auth/cloud-platform"}
tokenSource, err := google.JWTAccessTokenSourceWithScope(serviceAccountFileBytes, scopes...)
The reason that this one works, is because it creates the JWT token internally through the service_account's properties such as client email and client_id and private_key as GCP allows us to create our local JWT tokens and encode them.

How to implement authorization using Keycloak

I created a REST API in Go that is necessary an authorization layer, for this layer I am trying use Keycloak. The API will be consumed by a third-party backend service, anyone knows the workflow to integrate Go client and keycloak or already implemented it? I figured out an adapter called Gocloak but in its documentation there is not any example for this purpose.
Authorization is typically application specific, so I can't help much there, but here's some information on authenticating JWTs from Keycloak. After JWTs are authenticated, you can use their claims to authorize the request.
Keycloak exposes what's known as a JSON Web Key Set (JWKS). This resource should be used to authenticate JWTs. I've wrote a package for this purpose. It's an extension of github.com/golang-jwt/jwt/v4.
The package is called github.com/MicahParks/keyfunc. I've pasted the code example for Keycloak below.
package main
import (
"log"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/MicahParks/keyfunc"
)
func main() {
// Get the JWKS URL.
//
// This is a local Keycloak JWKS endpoint for the master realm.
jwksURL := "http://localhost:8080/auth/realms/master/protocol/openid-connect/certs"
// 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{
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.
jwtB64 := "eyJhbGciOiJQUzM4NCIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJMeDFGbWF5UDJZQnR4YXFTMVNLSlJKR2lYUktudzJvdjVXbVlJTUctQkxFIn0.eyJleHAiOjE2MTU0MDY5ODIsImlhdCI6MTYxNTQwNjkyMiwianRpIjoiMGY2NGJjYTktYjU4OC00MWFhLWFkNDEtMmFmZDM2OGRmNTFkIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL2F1dGgvcmVhbG1zL21hc3RlciIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiJhZDEyOGRmMS0xMTQwLTRlNGMtYjA5Ny1hY2RjZTcwNWJkOWIiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJ0b2tlbmRlbG1lIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwiY2xpZW50SG9zdCI6IjE3Mi4yMC4wLjEiLCJjbGllbnRJZCI6InRva2VuZGVsbWUiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6InNlcnZpY2UtYWNjb3VudC10b2tlbmRlbG1lIiwiY2xpZW50QWRkcmVzcyI6IjE3Mi4yMC4wLjEifQ.Rxrq41AxbWKIQHWv-Tkb7rqwel3sKT_R_AGvn9mPIHqhw1m7nsQWcL9t2a_8MI2hCwgWtYdgTF1xxBNmb2IW3CZkML5nGfcRrFvNaBHd3UQEqbFKZgnIX29h5VoxekyiwFaGD-0RXL83jF7k39hytEzTatwoVjZ-frga0KFl-nLce3OwncRXVCGmxoFzUsyu9TQFS2Mm_p0AMX1y1MAX1JmLC3WFhH3BohhRqpzBtjSfs_f46nE1-HKjqZ1ERrAc2fmiVJjmG7sT702JRuuzrgUpHlMy2juBG4DkVcMlj4neJUmCD1vZyZBRggfaIxNkwUhHtmS2Cp9tOcwNu47tSg"
// Parse the JWT.
token, err := jwt.Parse(jwtB64, jwks.Keyfunc)
if 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.
jwks.EndBackground()
}

AWS API Gateway WebSockets [POST]#connections Returning 404 NotFound

I connect a client (or a couple of clients) to the websockets endpoint in API Gateway.
Then, I try to post a message back to the client using these guidelines: https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html
// Send sends a message to a connection ID.
func Send(domain, stage, connectionID, message string) (events.APIGatewayProxyResponse, error) {
session := session.Must(session.NewSession())
endpoint := fmt.Sprintf("https://%s/%s/#connections/%s", domain, stage, connectionID)
apiClient := apigatewaymanagementapi.New(session, aws.NewConfig().WithEndpoint(endpoint))
connectionInput := apigatewaymanagementapi.PostToConnectionInput{
ConnectionId: aws.String(connectionID),
Data: []byte(message),
}
_, err := apiClient.PostToConnection(&connectionInput)
if err != nil {
fmt.Println(err.Error())
return events.APIGatewayProxyResponse{StatusCode: 500}, err
}
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
It doesn't matter whether I invoke the Send function locally or a client sends a message and API Gateway invokes my publish Lambda where I loop through the connections and invoke Send for each of them.
The result is always the same.
NotFoundException:
status code: 404, request id: 7bb1546a-c2a7-4e98-92a0-fcc7ae175d7c
Things I've tried:
Escaped #connections and the actual connectionID
Made sure the client connection hasn't timed out
Made sure I have the correct AWS credentials in my environment variables
Made sure my Lambda has permissions to invoke API Gateway
Made sure the endpoint is in the correct format: https://{api-id}.execute-api.{region}.amazonaws.com/{stage}/#connections/{connection_id}
How can I successfully send messages to the clients?
Turns out this line
endpoint := fmt.Sprintf("https://%s/%s/#connections/%s", domain, stage, connectionID)
needs to turn into this
endpoint := fmt.Sprintf("https://%s/%s/", domain, stage)

Google Calendar API invalid_grant getting token (Golang)

I'm trying to retrieve an access token, in order to authenticate users using Oauth2. I'm using mostly code found on google's HOW-TO page for using the Calendar API with golang. The problem is that whenever I try to obtain a token, google sends back this:
Response: {
"error" : "invalid_grant"
}
With the error oauth2: cannot fetch token: 400 Bad Request
As I said, I'm using some code got from google's howto, just slightly modified to fit my needs.
//Somewhere...
authURL = config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
//Somewhere else...
func getClient(ctx context.Context, config *oauth2.Config, code string) *http.Client {
cacheFile := tokenCacheFile()
tok, err := tokenFromFile(cacheFile)
if err != nil {
log.Printf("Google auth code not cached. Obtaining from the web...")
tok, err = getTokenFromWeb(code) //This returns an error
if err == nil {
log.Printf("Got token!")
saveToken("calendar-go-quickstart.json", tok)
} else { //Prevent saving token when error
log.Printf("Couldn't get OAUTH2 token! %s", err)
}
}
return config.Client(ctx, tok)
}
The error occurs at "getTokenFromWeb(code)" (if I understood correctly, code must be some random string, no matter its value, it just needs to be the same during the whole process).
This is the problematic code:
func getTokenFromWeb(code string) (*oauth2.Token, error) {
tok, err := config.Exchange(context.Background(), code)
return tok, err
}
After executing, what I see is that error. I even get the exact same error when trying to copy-paste google's own example code!
Any idea? I really can't find a solution online.
Extra details: using IRIS web framework; using the latest version of google calendar api; using the latest version of Golang; I've created a client ID for OAuth2 on Google Cloud Console; The website has got a trusted SSL cert; it listens on port 80 (HTTP) and 4433 (HTTPS);
Here is Google's example:
// getTokenFromWeb uses Config to request a Token.
// It returns the retrieved Token.
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
fmt.Printf("Go to the following link in your browser then type the "+
"authorization code: \n%v\n", authURL)
var code string
if _, err := fmt.Scan(&code); err != nil {
log.Fatalf("Unable to read authorization code %v", err)
}
...
}
code is an authorization code given to the user after visiting the displayed link. fmt.Scan() is going to scan the input from the user.
If you're going to be acting on a different user's behalf, you will have to do something similar to this example.
If you're only acting as yourself, you should be able to authenticate as yourself without the code.
Either way, code cannot be a random string.

Resources