gRPC Authentication with Keycloak in Go

A credentials.PerRPCCredentials implementation for authenticating with a Keycloak server to get an access token and submitting it with every RPC call.

gRPC features two types of authentication credentials: channel credentials which are attached to a channel, and call credentials which are attached to each call.

Channel credentials are TLS based and can be used either to just authenticate the server or for mutual authentication using a client TLS cert also. Call credentials are provided with every RPC call and typically use some kind of token to authenticate the caller.

The two types of credentials can also be combined. For example, the channel can be secured with a server certificate but the client can be authenticated with a token provided with each call. In this post, I describe one implementation of this hybrid authentication approach.

Below is a PerRPCCredentials implementation that uses gocloak to authenticate with a Keycloak server to get a JWT access token and submit it with every RPC call.

// KeycloakTokenAuth implements  PerRPCCredentials to include Keycloak OpenID
// Connect access tokens with every RPC call.
type KeycloakTokenAuth struct {
  // basePath is the URL of the Keycloak server; e.g.,
  // https://keycloak.example.com.
  basePath string
  // realm is the Keycloak realm.
  realm string
  // clientID is the Keycloak client ID.
  clientID string
  // clientSecret is the secret used for authentication when requesting
  // tokens from the Keycloak server.
  clientSecret string
  // client is the gocloak Keycloak client implementation.
  client gocloak.GoCloak
  // token is the gocloak structure holding tokens and metadata.
  token *gocloak.JWT
}

// NewKeycloakTokenAuth creates a KeycloakTokenAuth.
func NewKeycloakTokenAuth(
  basePath, realm, clientID, clientSecret string,
) (*KeycloakTokenAuth, error) {
  t := &KeycloakTokenAuth{
    basePath:     basePath,
    realm:        realm,
    clientID:     clientID,
    clientSecret: clientSecret,
  }
  t.client = gocloak.NewClient(t.basePath)
  token, err := t.client.LoginClient(t.clientID, t.clientSecret, t.realm)
  if err != nil {
    return nil, errors.Wrapf(err, "LoginClient")
  }
  t.token = token
  return t, nil
}

// GetRequestMetadata refreshes the token and add it as an authorization header.
func (t KeycloakTokenAuth) GetRequestMetadata(
  ctx context.Context, in ...string,
) (map[string]string, error) {
  token, err := t.client.RefreshToken(
    t.token.RefreshToken, t.clientID, t.clientSecret, t.realm,
  )
  if err != nil {
    return nil, errors.Wrapf(err, "RefreshToken")
  }
  t.token = token
  return map[string]string{
    "authorization": "Bearer " + t.token.AccessToken,
  }, nil
}

// RequireTransportSecurity always returns true.
func (KeycloakTokenAuth) RequireTransportSecurity() bool {
  return true
}

Client example:

// dial creates a KeycloakTokenAuth and uses it to dial target.
func dial(
  target, keycloakURL, realm, clientID, clientSecret string,
) (*grpc.ClientConn, error) {
  ta, err := NewKeycloakTokenAuth(keycloakURL, realm, clientID, clientSecret)
  if err != nil {
    return nil, errors.Wrapf(err, "NewKeycloakTokenAuth")
  }
  chanCreds := grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))
  callCreds := grpc.WithPerRPCCredentials(ta)
  conn, err := grpc.Dial(target, chanCreds, callCreds)
  if err != nil {
    return nil, errors.Wrapf(err, "grpc.Dial")
  }
  return conn, nil
}

func main() {
  conn, err := dial(
    "some-grpc-endpoint.example.com:5052",
    "https://keycloak.example.com",
    "master",
    "testing",
    os.Getenv("CLIENT_SECRET"),
  )
    ...
    ...
    ...
}

An “authorization: Bearer …” header is then included with every RPC request. The gRPC server can then verify the token before processing the request to authenticate the caller. Claims included in the token can then be used to authorize the request. The authentication and authorization on the server side can either be done in an interceptor or at the API gateway or service mesh level (e.g., Istio Policy).

Further reading: