Serverless Security Token Exchange Server(STS) and gRPC STS credentials

2020-11-04

This repo contains a very basic STS server deployed on Cloud Run which exchanges a one access_token for another….basically, a token broker described here:

The STS server here is not a reference implementation..just use this as a helloworld tutorial

This particular STS server exchanges one static access token for another. It will exchange

  • iamtheeggman for iamthewalrus (right, thats it..)

You can use an http client curl to see the exchange directly and then use a new gRPC client which utilizes its own gRPC STS Credential object:

This is not an officially supported Google product


You can find the source here


This tutorial will deploy

  • gRPC server on Cloud Run that will inspect the Authorization header and only allow the request if the value is iamthewalrus
  • STS server on Cloud Run that will only provide the token exchange if the inbound token is iamtheeggman
  • gRPC client that will use the STS Credential object to access the gRPC server after it performs the exchange.

then if you want

  • http_server locally which will inspect the Authoriztion header and only allow the request if the value is iamthewalrus
  • http_client locally that will use a custom STSTokenSource to get the new token

first gRPC

gRPC

Create a file with the original token:

mkdir /tmp/stscreds
echo -n iamtheeggman > /tmp/stscreds/creds.txt

Setup the environment variables and deploy the gRPC and STS servers to cloud run

export PROJECT_ID=`gcloud config get-value core/project`
export PROJECT_NUMBER=`gcloud projects describe $PROJECT_ID --format='value(projectNumber)'`

Deploy gRPC Server

cd server/
docker build -t gcr.io/$PROJECT_ID/grpc_server .
docker push gcr.io/$PROJECT_ID/grpc_server

gcloud beta run deploy grpcserver  \
  --image gcr.io/$PROJECT_ID/grpc_server \
  --allow-unauthenticated  --region us-central1  --platform=managed  -q

Deploy STS Server

cd sts_server/
# use cloud build
# gcloud builds submit --machine-type=n1-highcpu-8 --tag gcr.io/$PROJECT_ID/sts_server . 

# or directly
docker build -t gcr.io/$PROJECT_ID/sts_server .
docker push gcr.io/$PROJECT_ID/sts_server

gcloud run deploy stsserver  --image gcr.io/$PROJECT_ID/sts_server \
 --region us-central1  --allow-unauthenticated --platform=managed  -q

gRPC Client

First we need to find the assigned addresses for the gRPC server and the STS Server

cd client/

export GRPC_SERVER_ADDRESS=`gcloud run services describe grpcserver --format="value(status.url)"`
export GRPC_SERVER_ADDRESS=`echo "$GRPC_SERVER_ADDRESS" | awk -F/ '{print $3}'`
echo $GRPC_SERVER_ADDRESS

export STS_URL=`gcloud run services describe stsserver --format="value(status.url)"`/token
echo $STS_URL

go run grpc_client.go \
  --address $GRPC_SERVER_ADDRESS:443 \
  --cacert googleCA.crt \
  --servername $GRPC_SERVER_ADDRESS \
  --stsaddress $STS_URL \
  --usetls \
  --stsCredFile /tmp/stscreds/creds.txt

## note, you can get googleCA.crt by copying in `openssl s_client $GRPC_SERVER_ADDRESS:443`

ok, how does this work with gRPC? Well you just have to specify the STS Credential type as credential object with the specifications of the STS configurations.

In the command set below, we’re specifying the rest endpoint of the STS server, and critically the SubjectTokenPath which is the path to the file where we saved the source token.

Once you specify all this, the grpc Client will do the legwork to exchange the source token for the remote one

import (
  	"google.golang.org/grpc/credentials/sts"
)

		stscreds, err := sts.NewCredentials(sts.Options{
			TokenExchangeServiceURI: *stsaddress,
			Resource:                *stsaudience,
			Audience:                *stsaudience,
			Scope:                   *scope,
			SubjectTokenPath:        *stsCredFile,
			SubjectTokenType:        "urn:ietf:params:oauth:token-type:access_token",
			RequestedTokenType:      "urn:ietf:params:oauth:token-type:access_token",
		})

		conn, err = grpc.Dial(*address,
			grpc.WithTransportCredentials(ce),
			grpc.WithPerRPCCredentials(stscreds))    

…..now…why is specifying the SubjectTokenPath a file? I don’t know..at the very least it should be []byte or perhaps an oauth2.TokenSource which includes the value of the source token. I’ll file a bug about this. In the meantime, the code contained here is copy of the grpc sts.go file defines and uses SubjectTokenSource.

The usage for this in the client would look like

		stscreds, err := sts.NewCredentials(sts.Options{
			TokenExchangeServiceURI: *stsaddress,
			Resource:                *stsaudience,
			Audience:                *stsaudience,
			Scope:                   *scope,
			//SubjectTokenPath:        *stsCredFile,
			SubjectTokenSource: oauth2.StaticTokenSource(&oauth2.Token{
				AccessToken: "iamtheeggman",
				Expiry:      time.Now().Add(time.Duration(300 * time.Second)),
			}),			
			SubjectTokenType:        "urn:ietf:params:oauth:token-type:access_token",
			RequestedTokenType:      "urn:ietf:params:oauth:token-type:access_token",
		})

Alternatively, just to test,you can inject a test TokenSource instead of the static on.

curl

If you just want to see the formats accepted by the STS server, you can use the commands shown below

  • as application/x-www-form-urlencoded
$ cd curl/
$ curl -s -H "Content-Type: application/x-www-form-urlencoded"  -d @sts_req.txt  $STS_URL | jq '.'
{
  "access_token": "iamthewalrus",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 60
}
  • as application/json
$ cd curl/
$ curl -s -X POST -H "Content-Type: application/json" -d @sts_req.json  $STS_URL | jq '.'
{
  "access_token": "iamthewalrus",
  "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "token_type": "Bearer",
  "expires_in": 60
}

Note the two source files contains the specifications of the source token that will get sent to the STS server

{
    "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
    "resource": "grpcserver-6w42z6vi3q-uc.a.run.app",
    "audience": "grpcserver-6w42z6vi3q-uc.a.run.app",
    "requested_token_type": "urn:ietf:params:oauth:token-type:access_token",
    "subject_token": "iamtheeggman",
    "subject_token_type": "urn:ietf:params:oauth:token-type:access_token"
}

If you change the value of the subject_token, the STS server will reject the request.

if you want to, edit the files and replace the values for resource and audience with $GRPC_SERVER_ADDRESS (its a no-op since this isn’t used in this STS server )


STSTokenSource Server demo using HTTP Client

The following bootstraps STS Credentials from and to an oauth2.TokenSource.

This basically means you can inject any token into a TokenSource and then utilize an unsupported library here: https://github.com/salrashid123/oauth2#usage-sts

to derive a new tokensource based off an STS server.

I’m using an oauth2 token source here but clearly, the source and target tokensources need not be oauth2.

this is not supported by google and neither is github.com/salrashid123/oauth2/sts

You must first deploy the STS Server into cloud run as shown in the root repo.

Start HTTP Server

cd http_server
go run server.go

Start Client

$ go run client.go --stsaddress https://stsserver-6w42z6vi3q-uc.a.run.app/token

$ go run client.go --stsaddress https://stsserver-6w42z6vi3q-uc.a.run.app/token
		myToken not valid refreshing 
		myToken, returning new [iamtheeggman]
		2021/08/11 21:35:36 New Token: iamthewalrus
		myToken not valid refreshing 
		myToken, returning new [iamtheeggman]
		2021/08/11 21:35:36 ok

The following snippet shows the exchange happening from a source token “iamtheeggman” to a destination tokensource that will automatically perform the STS Exchange.

One the exchange takes place, a plain authorized request

import (
   	sal "github.com/salrashid123/oauth2/sts"
)

	client := &http.Client{}

    // start with a source tken
	rootTS := oauth2.StaticTokenSource(&oauth2.Token{
		AccessToken: "iamtheeggman",
		TokenType:   "Bearer",
		Expiry:      time.Now().Add(time.Duration(time.Second * 60)),
    })
    
    // exchange it
	stsTokenSource, _ := sal.STSTokenSource(
		&sal.STSTokenConfig{
			TokenExchangeServiceURI: "https://stsserver-6w42z6vi3q-uc.a.run.app/token",
			Resource:                "localhost",
			Audience:                "localhost",
			Scope:                   "https://www.googleapis.com/auth/cloud-platform",
			SubjectTokenSource:      rootTS,
			SubjectTokenType:        "urn:ietf:params:oauth:token-type:access_token",
			RequestedTokenType:      "urn:ietf:params:oauth:token-type:access_token",
		},
    )
    
    // print the new token (iamthewalrus)
	tok, err := stsTokenSource.Token()
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("New Token: %s", tok.AccessToken)    

    // use the new token
	client = oauth2.NewClient(context.TODO(), stsTokenSource)
	resp, err := client.Get("http://localhost:8080/")
	if err != nil {
		log.Printf("Error creating client %v", err)
		return
    }

This site supports webmentions. Send me a mention via this form.