I work on go grpc service and implementing authorization. Literally, have to allow or forbid access to gprc methods base on JWT claims.
I do JWT parsing on grpc.UnaryServerInterceptor level - extracting claims and populate context with value, unauthenticated if there is no jwt or it is incorrect.
func (s *Server) GetSomething(ctx context.Context, req *GetSomething Request) (*GetSomething Response, error) {
if hasAccessTo(ctx, req.ID) {
//some work here
}
}
func hasAccessTo(ctx context.Context, string id) {
value := ctx.Value(ctxKey).(MyStruct)
//some work here
}
So I wonder if there is some common practice for authorization/authentication to avoid boilerplate code in each grpc server method?
You can call a to a UnaryInterceptor like so if you want to verify the jwt on every request
// middleware for each rpc request. This function verifies the client has the correct "jwt".
func authInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
meta, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "INTERNAL_SERVER_ERROR")
}
if len(meta["jwt"]) != 1 {
return nil, status.Error(codes.Unauthenticated, "INTERNAL_SERVER_ERROR")
}
// if code here to verify jwt is correct. if not return nil and error by accessing meta["jwt"][0]
return handler(ctx, req) // go to function.
}
In your context from the client use the metadata to pass the jwt string and verify.
In Your main function remember to register it like so
// register server
myService := grpc.NewServer(
grpc.UnaryInterceptor(authInterceptor), // use auth interceptor middleware
)
pb.RegisterTheServiceServer(myService, &s)
reflection.Register(myService)
Your client would need to call your server like this:
// create context with token and timeout
ctx, cancel := context.WithTimeout(metadata.NewOutgoingContext(context.Background(), metadata.New(map[string]string{"jwt": "myjwtstring"})), time.Second*1)
defer cancel()
Related
I have a custom handler for my API endpoints like this:
type HTTPError struct {
Error error
Message string
Code int
}
type endpointREST func(http.ResponseWriter, *http.Request) *HTTPError
func (fn endpointREST) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil {
http.Error(w, e.Message, e.Code)
}
}
my example route look like this:
func GetShare(w http.ResponseWriter, r *http.Request) *HTTPError {
vars := mux.Vars(r)
fmt.Println(r.URL) // http://127.0.0.1:36455/share/5713d228-a042-446d-a5e4-183b19fa832a
fmt.Println(vars) // -->> always empty map when testing
return nil
}
These routes work well (manual, using Postman) after setting them up with
router := mux.NewRouter().StrictSlash(true)
handler := cors.Default().Handler(router)
router.Handle("/share/{id}", endpointREST(GetShare)).Methods("GET")
log.Fatal(http.ListenAndServe(":6969", handler))
The Problem is, that i can't test the API this way, since mux.Vars(r) will always return an empty map in the testing environment.
This is my testing code:
func TestGetShare(t *testing.T) {
Reset()
router := mux.NewRouter()
ts := httptest.NewServer(router)
router.Handle("/share/{id}", endpointREST(GetShare)).Methods("GET")
defer ts.Close()
t.Run("unauthorized", func(t *testing.T) {
req, _ := http.NewRequest("GET", ts.URL + "/share/5713d228-a042-446d-a5e4-183b19fa832a", nil)
res, _ := http.DefaultClient.Do(req)
assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
})
}
I suggest you tu use the SetURLVars test helper func:
// SetURLVars sets the URL variables for the given request, to be accessed via
// mux.Vars for testing route behaviour. Arguments are not modified, a shallow
// copy is returned.
//
// This API should only be used for testing purposes; it provides a way to
// inject variables into the request context. Alternatively, URL variables
// can be set by making a route that captures the required variables,
// starting a server and sending the request to that server.
func SetURLVars(r *http.Request, val map[string]string) *http.Request {
return requestWithVars(r, val)
}
I wanted to play around with Spring reactive web client and an actually simple example: Ask for a REST resource and in case of a 401 response get new OAuth access token.
The first part seemed to be easy:
return webClientBuilder
.baseUrl(targetInstance.getBaseUrl())
.build()
.get().uri(targetInstance.getItemEndpointUrl())
.retrieve()
.bodyToMono(ItemResponse.class)
....
But here the confusion already started. I tried something like
.onStatus(HttpStatus::is4xxClientError, (response) -> {
if(response.rawStatusCode() == 401) {
oAuthClient.initToken()
My token should then be saved within an instance JPA entity. But I have a lack of conceptual understanding here I guess. When the OAuth client receives the OAuth response I need to extract it first to persist it (as embedded object) within my instance entity. And therefore I need to block it, right?
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
OAuthResponse oauthResponse = response.bodyToMono(OAuthResponse.class).block();
}
Based on the response result of the OAuth client I need some kind of Mono to tell the actual REST client then if it should start a retry? And which way should be the preferred on: .retrieve() or .exchangeToMono()? So I'm a bit lost here if I'm on the right path or if something like that should better be done with the classic RestTemplate? But I've also read that the RestTemplate is no deprecated...
Thanks for sharing some thoughts with me.
Ok, in the meantime I've found a non-blocking way. Maybe not the best, but it works out well for me.
The client:
class ApiClient {
public Mono<MyResponse> getResponse(Tenant tenant) {
return webClientBuilder
.baseUrl(tenant.getUrl())
.clientConnector(getClientConnector())
.build()
.get().uri("/api/my-content-entpoint")
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(MyResponse.class);
} else if(response.statusCode().equals(HttpStatus.FORBIDDEN)) {
return Mono.error(new MyOAuthExcpetion());
} else {
return Mono.empty();
}
});
}
}
the service:
#Service
public class MyService {
private final ApiClient apiClient;
private final RetryStrategy retryStrategy;
private final TenantService tenantService;
public Mono<MyResponse> getResponse(String tenantId){
return tenantService.getTenant(tenantId)
.flatMap(tenant-> apiClient.getResponse(instance))
.retryWhen(Retry.from(signals -> signals
.flatMap(retrySignal -> retryStrategy.reconnect(retrySignal, tenantId))));
}
}
and the retry strategy
#Component
public class RetryStrategy {
private final TenantService tenantService;
public Publisher<? extends Long> reconnect(RetrySignal retrySignal, String tenantId) {
long count = retrySignal.totalRetriesInARow();
Throwable failure = retrySignal.failure();
if(count > 0) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
Mono<Tenant> updatedTenant = null;
if(failure instanceof MyOAuthExcpetion) {
updatedTenant = tenantService.getTenant(tenantId)
.flatMap(tenant -> tenantService.refreshOAuth(tenant));
}
if(updatedTenant == null) {
return Mono.error(new UnsupportedOperationException("Retry failed", failure));
}
return updatedTenant.then(Mono.delay(Duration.ofSeconds(1)));
}
}
Happy for any feedback or improvements.
In my application I went with prechecking the token before requests are being made:
client.get()
.uri("...")
.header("Authorization", "Bearer " + authenticator.getToken(client,token))
.retrieve()
...
And in Authenticator Service I verify the validity of the token as follow:
String getToken(WebClient client, String token) {
if (token == null || isTokenExpired(token)) {
return this.fetchToken(client); // fetches a new token
}
return token;
}
private boolean isTokenExpired(String token) {
DecodedJWT jwt = JWT.decode(token);
return jwt.getExpiresAt().before(new Date());
}
package main
import (
"fmt"
"github.com/softlayer/softlayer-go/services"
"github.com/softlayer/softlayer-go/session"
)
func main() {
// SoftLayer API username and key
username := "my-username" // used actual username and api-key
apikey := "My-APIkey"
// Create SoftLayer API session
sess := session.New(username, apikey)
// Get SoftLayer_Account service
resp := services.GetUserCustomerApiAuthenticationService(sess)
users, err := resp.GetUser()
if err != nil {
fmt.Printf("\n Unable to get users:\n - %s\n", err)
return
}
fmt.Printf("\n Unable to get users:\n - %s\n", users)
}
=====
$ ./list_user
Unable to get users:
- SoftLayer_Exception: Object does not exist to execute method on. (SoftLayer_User_Customer_ApiAuthentication::getUser) (HTTP 500)
I tried the similar code to get object as well. It throws same exception.
The reason you are getting this error is due to that both methods need to use an User_Customer_ApiAuthentication identifier.
You can use the following code on both methods as they will use the same authentication id:
package main
import (
"fmt"
"encoding/json"
"github.com/softlayer/softlayer-go/services"
"github.com/softlayer/softlayer-go/session"
)
func main() {
// SoftLayer API username and key
username := "my-username" // used actual username and api-key
apikey := "My-ApiKey"
// Create SoftLayer API session
sess := session.New(username, apikey)
// Get SoftLayer_Account service
resp := services.GetUserCustomerApiAuthenticationService(sess)
users, err := resp.Id(123456).GetUser()
if err != nil {
fmt.Printf("\n Unable to get users:\n - %s\n", err)
return
}
//Following helps to print the result in json format.
jsonFormat, jsonErr := json.MarshalIndent(users,""," ")
if jsonErr != nil {
fmt.Println(jsonErr)
return
}
fmt.Println(string(jsonFormat))
}
If you need to retrieve the authentication id above, you can use the SoftLayer_User_Customer::getObject method and search for the apiAuthenticationKeys relational property that will contain it.
I am new to GO and APIs and I am making a back end using GO.
the user should be able to login using his/her google account and modifies his calendar.
I opened the sample on this link Google Quickstart
but the way I get the client is by the keys google gives it to me
how should I make the user login and get his calendar
You'll need to do something like:
import (
"crypto/rand"
"encoding/base64"
"encoding/gob"
"golang.org/x/oauth2/google"
"golang.org/x/oauth2"
calendar "google.golang.org/api/calendar/v3"
"github.com/gorilla/sessions"
)
var conf oauth2.Config
func init() {
gob.Register(&oauth2.Token{})
}
func getLoginURL(state string) string {
// State can be some kind of random generated hash string.
// See relevant RFC: http://tools.ietf.org/html/rfc6749#section-10.12
return conf.AuthCodeURL(state)
}
func randToken() string {
b := make([]byte, 32)
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
func Login(w http.ResponseWriter, r *http.Request) {
conf = &oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
RedirectUrl: "https://www.yoursite.com/auth",
Endpoint: google.Endpoint,
Scopes: []string{"https://www.googleapis.com/auth/calendar"}
}
state := randToken()
sess, _ := session.Get(r, "session")
sess.Values["state"] = state
sess.Save(r, w)
http.Redirect(w, r, conf.AuthCodeURL(state), http.StatusFound)
}
func Auth(w http.ResponseWriter, r *http.Request) {
sess, _ := session.Get(r, "session")
state = sess.Values["state"]
if state != r.URL.Query().Get("state") {
http.Error(w, "authorization failed", http.StatusUnauthorized)
return
}
tok, _ := conf.Exchange(oauth2.NoContext, c.QueryParam("code"))
sess.Values["token"] = tok
sess.Save(r, w)
http.Redirect(w, r, "https://www.yoursite.com/profile", http.StatusFound)
}
func GetClient(r *http.Request) *http.Client {
sess, _ := session.Get(r, "session")
tok, _ := sess.Values["token"].(*oauth2.Token)
client := conf.Client(oauth2.NoContext, tok)
return client
}
func Calendar(w http.ResponseWriter, r *http.Request) {
client := GetClient(r)
calendarService, _ = calendar.New(client)
//do stuff
}
So, you send them to your Login handler, this generates a random key, and sends it (and the user) to google to have them login and authorize you to access their calendars, which will then redirect them to you Auth handler. This will make sure that the state key they sent back matches the one you sent, and if so, will get the token from Google. You then save it to the session. When you want to get their client, you fetch the token from your session, and use it to exchange for a new Client, which you then use to create your Calendar service.
I haven't checked the code exactly, but I tried to make a minimal example from my app which actually uses basically this code, so it should work (aside from probably missing an import or typos or some really minor stuff).
I am currently building a web application in golang (with Gorilla) and have implemented a handful of API endpoints. However, I noticed that every time I implement a function like
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {}
I have to add the function below to the body of handler functions to check if request is authorized:
func checkAuthorizedUser (r * http.Request) error {
uid, err := CheckRequestUser (r.Cookie("uid"))
if err != nil {
return errors.New("Can't find cookie value for uid")
}
if !IsValidUser (uid.Value) {
return errors.New("Not a valid user")
}
return nil
}
What happens to me right now is that I have to add checkAuthorizedUser() to every handler function, and I have already have a lot of handler functions so far. I wonder if there is a better way to check whether a client is authorized to access certain endpoint other than explicitly checking authentication in every handler function.
Gorilla has a router you can use. You can then wrap the router with authentication checking. Something like this would work:
func checkPermissions(h http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
authCheck := true //implement the actual checking
if authCheck {
w.WriteError(w, 400, "error")
return
}
h.ServeHttp(w, r)
}
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/", HomeHandler)
r.HandleFunc("/products", ProductsHandler)
r.HandleFunc("/articles", ArticlesHandler)
http.Handle("/", checkPermissions(r))
}
Supporting links:
https://godoc.org/github.com/gorilla/mux#NewRouter
https://github.com/gorilla/mux