jwt: add the (last) helper: VerifyRefreshToken

This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-10-18 17:15:29 +03:00
parent 09923183e8
commit 0d73b63b28
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
3 changed files with 97 additions and 32 deletions

View File

@ -60,7 +60,8 @@ func main() {
// http://localhost:8080/protected?token={access_token} (200) // http://localhost:8080/protected?token={access_token} (200)
// http://localhost:8080/protected?token={refresh_token} (401) // http://localhost:8080/protected?token={refresh_token} (401)
// http://localhost:8080/refresh?token={refresh_token} // http://localhost:8080/refresh?token={refresh_token}
// OR (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token}) // OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
// OR http://localhost:8080/refresh (request PLAIN TEXT of {refresh_token}) (200) (response JSON {access_token, refresh_token})
// http://localhost:8080/refresh?token={access_token} (401) // http://localhost:8080/refresh?token={access_token} (401)
app.Listen(":8080") app.Listen(":8080")
} }
@ -95,45 +96,36 @@ func generateTokenPair(ctx iris.Context, j *jwt.JWT) {
} }
func refreshToken(ctx iris.Context, j *jwt.JWT) { func refreshToken(ctx iris.Context, j *jwt.JWT) {
var tokenPair jwt.TokenPair /*
We could pass a jwt.Claims pointer as the second argument,
but we don't have to because the method already returns
the standard JWT claims information back to us:
refresh, err := VerifyRefreshToken(ctx, nil)
*/
if token := ctx.URLParam("token"); token != "" { // Assuming you have access to the current user, e.g. sessions.
// Grab the refresh token from the url argument. //
tokenPair.RefreshToken = token // Simulate a database call against our jwt subject
} else { // to make sure that this refresh token is a pair generated by this user.
// Otherwise grab the refresh token from a JSON body (you can let it fetch by URL parameter too but // * Note: You can remove the ExpectSubject and do this validation later on by yourself.
// it's common practice that you read it from a json body as currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
// it may contain the access token too (the same response we sent on generateTokenPair)).
err := ctx.ReadJSON(&tokenPair)
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
return
}
}
var refreshClaims jwt.Claims // Verify the refresh token, which its subject MUST match the "currentUserID".
_, err := j.VerifyTokenString(ctx, tokenPair.RefreshToken, &refreshClaims, jwt.ExpectRefreshToken) _, err := j.VerifyRefreshToken(ctx, nil, jwt.ExpectSubject(currentUserID))
if err != nil { if err != nil {
ctx.Application().Logger().Debugf("verify refresh token: %v", err) ctx.Application().Logger().Debugf("verify refresh token: %v", err)
ctx.StatusCode(iris.StatusUnauthorized) ctx.StatusCode(iris.StatusUnauthorized)
return return
} }
// Assuming you have access to the current user, e.g. sessions. /* Custom validation checks can be performed after Verify calls too:
//
// Simulate a database call against our jwt subject
// to make sure that this refresh token is a pair generated by this user.
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149" currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
userID := refresh.Claims.Subject
userID := refreshClaims.Subject
if userID != currentUserID { if userID != currentUserID {
ctx.StopWithStatus(iris.StatusUnauthorized) ctx.StopWithStatus(iris.StatusUnauthorized)
return return
} }
// */
// Otherwise, the request must contain the (old) access token too,
// even if it's invalid, we can still fetch its fields, such as the user id.
// [...leave it for you]
// All OK, re-generate the new pair and send to client. // All OK, re-generate the new pair and send to client.
generateTokenPair(ctx, j) generateTokenPair(ctx, j)

View File

@ -2410,7 +2410,28 @@ func (ctx *Context) ReadMsgPack(ptr interface{}) error {
// If a GET method request then it reads from a form (or URL Query), otherwise // If a GET method request then it reads from a form (or URL Query), otherwise
// it tries to match (depending on the request content-type) the data format e.g. // it tries to match (depending on the request content-type) the data format e.g.
// JSON, Protobuf, MsgPack, XML, YAML, MultipartForm and binds the result to the "ptr". // JSON, Protobuf, MsgPack, XML, YAML, MultipartForm and binds the result to the "ptr".
// As a special case if the "ptr" was a pointer to string or []byte
// then it will bind it to the request body as it is.
func (ctx *Context) ReadBody(ptr interface{}) error { func (ctx *Context) ReadBody(ptr interface{}) error {
// If the ptr is string or byte, read the body as it's.
switch v := ptr.(type) {
case *string:
b, err := ctx.GetBody()
if err != nil {
return err
}
*v = string(b)
case *[]byte:
b, err := ctx.GetBody()
if err != nil {
return err
}
copy(*v, b)
}
if ctx.Method() == http.MethodGet { if ctx.Method() == http.MethodGet {
if ctx.Request().URL.RawQuery != "" { if ctx.Request().URL.RawQuery != "" {
// try read from query. // try read from query.

View File

@ -395,6 +395,32 @@ func (j *JWT) VerifyToken(ctx *context.Context, claimsPtr interface{}, expectati
return j.VerifyTokenString(ctx, token, claimsPtr, expectations...) return j.VerifyTokenString(ctx, token, claimsPtr, expectations...)
} }
// VerifyRefreshToken like the `VerifyToken` but it verifies a refresh token one instead.
// If the implementation does not fill the application's requirements,
// you can ignore this method and still use the `VerifyToken` for refresh tokens too.
//
// This method adds the ExpectRefreshToken expectation and it
// tries to read the refresh token from raw body or,
// if content type was application/json, then it extracts the token
// from the JSON request body's {"refresh_token": "$token"} key.
func (j *JWT) VerifyRefreshToken(ctx *context.Context, claimsPtr interface{}, expectations ...Expectation) (*TokenInfo, error) {
token := j.RequestToken(ctx)
if token == "" {
var tokenPair TokenPair // read "refresh_token" from JSON.
if ctx.GetContentTypeRequested() == context.ContentJSONHeaderValue {
ctx.ReadJSON(&tokenPair) // ignore error.
token = tokenPair.RefreshToken
if token == "" {
return nil, ErrMissing
}
} else {
ctx.ReadBody(&token)
}
}
return j.VerifyTokenString(ctx, token, claimsPtr, append(expectations, ExpectRefreshToken)...)
}
// RequestToken extracts the token from the request. // RequestToken extracts the token from the request.
func (j *JWT) RequestToken(ctx *context.Context) (token string) { func (j *JWT) RequestToken(ctx *context.Context) (token string) {
for _, extract := range j.Extractors { for _, extract := range j.Extractors {
@ -536,10 +562,34 @@ func (j *JWT) VerifyTokenString(ctx *context.Context, token string, dest interfa
tokenMaxAger tokenWithMaxAge tokenMaxAger tokenWithMaxAge
) )
if err = parsedToken.Claims(j.VerificationKey, dest, &claims, &tokenMaxAger); err != nil { var (
ignoreDest = dest == nil
ignoreVarClaims bool
)
if !ignoreDest { // if dest was not nil, check if the dest is already a standard claims pointer.
_, ignoreVarClaims = dest.(*Claims)
}
// Ensure read the standard claims one if dest was Claims or was nil.
// (it wont break anything if we unmarshal them twice though, we just do it for performance reasons).
var pointers = []interface{}{&tokenMaxAger}
if !ignoreDest {
pointers = append(pointers, dest)
}
if !ignoreVarClaims {
pointers = append(pointers, &claims)
}
if err = parsedToken.Claims(j.VerificationKey, pointers...); err != nil {
return nil, err return nil, err
} }
// Set the std claims, if missing from receiver so the expectations and validation still work.
if ignoreVarClaims {
claims = *dest.(*Claims)
} else if ignoreDest {
dest = &claims
}
expectMaxAge := j.MaxAge expectMaxAge := j.MaxAge
// Build the Expected value. // Build the Expected value.
@ -594,10 +644,12 @@ func (j *JWT) VerifyTokenString(ctx *context.Context, token string, dest interfa
} }
} }
if ut, ok := dest.(TokenSetter); ok { if !ignoreDest {
// The u.Token is empty even if we set it and export it on JSON structure. if ut, ok := dest.(TokenSetter); ok {
// Set it manually. // The u.Token is empty even if we set it and export it on JSON structure.
ut.SetToken(token) // Set it manually.
ut.SetToken(token)
}
} }
// Set the information. // Set the information.