Issue JWT from lambda

I was messing around with dgraph lambdas for a side project and wanted to issue JWTs from this lambda (more or less to hide the secret used to sign with) and it took me a long time to get something to work. I am mostly a golang programmer these days so javascript was super annoying for me… but I wanted to share what I had in case it helps someone else save a few hours.

(disclaimer: no warranty, no support, no liability, not security advice)

const hdr = {"alg": "HS256","typ": "JWT"}
const tokenExpirySeconds = 900

// not to be registered directly as a lambda but called from a lambda registered function
async function CreateAndSignJWTWithClaims(claims, secretKey) {

   const now = ~~(Date.now()/1000)
   claims.iat=now
   claims.exp=now+tokenExpirySeconds

   const unsignedToken = base64url(JSON.stringify(hdr)) + "." + base64url(JSON.stringify(claims))
   const key = await crypto.subtle.importKey(
      "raw",
      new TextEncoder().encode(secretKey),
      {name: "HMAC", hash: "SHA-256" },
      false,
      ["sign"]
   )
   const sig = await crypto.subtle.sign(
      {name:"HMAC"},
      key,
      new TextEncoder().encode(unsignedToken)
   )
   return unsignedToken + "." + base64url(_arrayBufferToBinary(sig))
}

// this is used to change standard base64 to jwt-ready base64. Adapted from stackoverflow
function base64url(source) {
   encodedSource = btoa(source)
   encodedSource = encodedSource.replace(/=+$/, '')
   encodedSource = encodedSource.replace(/\+/g, '-')
   encodedSource = encodedSource.replace(/\//g, '_')
   return encodedSource
}

// this is modified from stackoverflow "how to turn an ArrayBuffer into an Uint8Array"
// I really hope there is some other way to do this....
function _arrayBufferToBinary( buffer ) {
   var binary = '';
   var bytes = new Uint8Array( buffer );
   var len = bytes.byteLength;
   for (var i = 0; i < len; i++) {
      binary += String.fromCharCode( bytes[ i ] );
   }
   return binary ;
}

Again, I do not know if there is a better way to do this, but I wanted a thing that would sign a jwt without using webpack. Sorry if there is already a better snippet out there that does this. The above will make a JWT that is usable with dgraph given the secretKey given as the VerificationKey in the annotation:

# Dgraph.Authorization {"VerificationKey":"SECRETKEY","Algo":"HS256"}

Also, for the record, javascript is infuriating.

Yes. There is a community made golang lambda server available. You might want to use that instead.

@Schartey Are you still working actively on that project?

yea I made the project expecting the $10/mo shared instance of dgraph cloud and using the built-in lambda support, but now that is $40 and I probably should have just went with golang from the get-go.

But yea, super cool library, thanks @Schartey

Sorry for the late response. I was a little occupied in the last few months.
I have been working on the project again and made a lot of breaking changes as you can see here Lambda Server in Go - #7 by Schartey

I would be really glad if you could leave some feedback if you end up using the library. I have still a lot of testing to do, but the basic features should be working fine as I am also using it in my own project.

In terms of JWT generation, here is an example when using my lambda server:

Schema:

type AuthResponse  @generate(
    query: {
        get: false,
        query: false,
        aggregate: false
    },
    mutation: {
        add: false,
        delete: false
    },
    subscription: false
) {
    user: User
    token: String
    expiration: DateTime
}

type Mutation {
    login(username: String, password: String): AuthResponse! @lambda
}

JWT generation in generated login mutation resolver using github.com/dgrijalva/jwt-go

func (q *MutationResolver) Mutation_login(ctx context.Context, username string, password string, authHeader api.AuthHeader) (*model.AuthResponse, *api.LambdaError) {
	expiration := time.Now().Add(time.Minute * 15)
	
	atClaims := jwt.MapClaims{}
	atClaims["exp"] = expiration.Unix()
	atClaims["authenticated"] = "true" // Make this a string for authorization rules in dgraph

	user, err := ... // Query the user
	if err != nil {
		return nil, &api.LambdaError{Underlying: err, Status: api.NOT_FOUND}
	}

	token, err := at.SignedString([]byte("secret"))
	if err != nil {
		return nil, &api.LambdaError{Underlying: err, Status: api.INTERNAL_ERROR}
	}
	return &model.AuthResponse{User: user, Token: token, Expiration: &expiration}, nil
}

This is quickly copied together, there might be some errors, but it should be enough to get going.