26 How is the IAM project designed and implemented for access authentication? #
Hello, I am Kong Lingfei.
In the previous lesson, we learned about four common methods of application authentication: Basic, Digest, OAuth, and Bearer. In this lesson, let’s take a look at how the IAM project is designed and implemented for authentication.
The IAM project uses Basic authentication and Bearer authentication. Specifically, Basic authentication is used for frontend login scenarios, while Bearer authentication is used for calling backend API services.
Next, let’s first look at the overall design concept of the authentication function in the IAM project.
26 IAM Project How to Design and Implement Access Authentication Functionality #
Before developing the authentication functionality, we need to carefully consider how to design the authentication functionality based on the requirements and conduct a technical review during the design phase. So let’s first look at how to design the authentication functionality for IAM projects.
Firstly, we need to clarify the use cases and requirements for the authentication functionality.
- The
iam-apiserver
service of the IAM project provides the management flow function interface of the IAM system. Its clients can be front-end (also known as a console) or app-side. - In order to facilitate calling on Linux systems, the IAM project also provides the
iamctl
command-line tool. - To support calling the API interfaces provided by
iam-apiserver
in third-party code, API calls are also supported. - In order to improve the efficiency of calling API interfaces in code, the IAM project provides a Go SDK.
From the above, we can see that iam-apiserver
has many clients, and the authentication methods used by each client are different.
The console and app-side require logging in to the system, so they need to use the username: password
authentication method, also known as Basic authentication. iamctl
, API calls, and Go SDK do not require logging in to the system, so a more secure authentication method, Bearer authentication, can be used. However, Basic authentication, as an integrated authentication method of iam-apiserver
, can still be used by iamctl
, API calls, and Go SDK.
One thing to note here: if iam-apiserver
uses Bearer Token authentication, the most popular token format currently is JWT Token. And JWT Token requires a secret key (referred to as secretKey
here) which needs to be maintained for each user in the iam-apiserver
service, which increases development and maintenance costs.
There is a better implementation approach in the industry: register the API interfaces provided by iam-apiserver
to an API gateway to implement authentication for the iam-apiserver
API interfaces through the token authentication function of the API gateway. There are many choices for API gateways, such as Tencent Cloud API Gateway, Tyk, Kong, etc.
Here, please note that the key pairs created by iam-apiserver
are for use by iam-authz-server
.
In addition, we need to call the RESTful API interface provided by iam-authz-server
: /v1/authz
, to perform resource authorization. The Bearer authentication method is suitable for API calls.
Of course, /v1/authz
can also be directly registered to the API gateway. In actual Go project development, this is also the approach I recommend. However, in this case, in order to demonstrate the process of implementing Bearer authentication, iam-authz-server
implements Bearer authentication itself. I will explain this in detail when I talk about the implementation of Bearer authentication by iam-authz-server
.
Basic authentication requires a username and password, while Bearer authentication requires a secret key. Therefore, iam-apiserver
needs to save username/password, secret keys, and other information in the backend MySQL for persistent storage.
When performing authentication, it is necessary to retrieve and decrypt the password or secret key. There are two ways to query the password or secret key. One way is to query the database when the request arrives. Because the database query operation has high latency, it will lead to a high delay of the API interface, so it is not suitable for use in data flow components. Another way is to cache the password or secret key in memory so that when a request arrives, it can be directly queried from memory, thus improving query speed and API performance.
However, when caching the password or secret key in memory, the consistency between memory and the database needs to be considered, which increases the complexity of code implementation. Because control flow components are not so sensitive to performance latency requirements, while data flow components must achieve very high API performance, iam-apiserver
queries the database when the request arrives, while iam-authz-server
caches the key information in memory.
Here, we can summarize the authentication design diagram of an IAM project:
In addition, in order to distinguish between control flow and data flow, the CRUD operations for keys are also placed in iam-apiserver
, but iam-authz-server
needs to use this key information. To solve this problem, the current approach is:
iam-authz-server
requests all key information fromiam-apiserver
through the gRPC API.- When
iam-apiserver
has key updates, it will publish a message to the Redis Channel. Sinceiam-authz-server
subscribes to the same Redis Channel,iam-authz-server
listens to new messages on the channel, retrieves and parses the messages, and updates the cached key information. This way, we can ensure that the key information cached iniam-authz-server
memory remains consistent with the key information iniam-apiserver
.
At this point, you might ask: if all keys are cached in iam-authz-server
, wouldn’t it take up a lot of memory? Don’t worry, I have considered this problem and calculated it for you: 8GB of memory can approximately store about 80 million key information, which is completely enough. If it is insufficient in the later stage, you can increase the memory.
However, there is still a small flaw here: if Redis is down or there is network jitter, it may cause the key data stored in iam-apiserver
and iam-authz-server
memory to be inconsistent. But this does not prevent us from learning about the design and implementation of authentication functionality. As for how to ensure data consistency in the cache system, I will specifically introduce it in a special edition in the future.
Finally, one important note: both Basic authentication requests and Bearer authentication requests can be intercepted and replayed. Therefore, to ensure the security of Basic authentication and Bearer authentication, it is necessary to use the HTTPS protocol when communicating with the server.
How does the IAM project implement Basic authentication? #
As we know, the IAM project mainly uses Basic and Bearer authentication methods. We need to support both Basic authentication and Bearer authentication, and choose different authentication methods as needed. This naturally makes us think of using the Strategy design pattern to implement it. Therefore, in the IAM project, I treat each authentication method as a strategy, and use different strategies to employ different authentication methods.
The IAM project implements the following strategies:
- auto strategy: This strategy selects either Basic authentication or Bearer authentication based on the HTTP headers
Authorization: Basic XX.YY.ZZ
andAuthorization: Bearer XX.YY.ZZ
. - basic strategy: This strategy implements Basic authentication.
- jwt strategy: This strategy implements Bearer authentication, where JWT is the specific implementation of Bearer authentication.
- cache strategy: This strategy is actually an implementation of Bearer authentication, where the token uses the JWT format. It is called Cache authentication because the key ID in the token is retrieved from memory. We will provide a detailed explanation of this later.
The IAM API server achieves API authentication by creating the required authentication strategies and loading them onto the API routes that need authentication. The specific code is as follows:
jwtStrategy, _ := newJWTAuth().(auth.JWTStrategy)
g.POST("/login", jwtStrategy.LoginHandler)
g.POST("/logout", jwtStrategy.LogoutHandler)
// Refresh time can be longer than token timeout
g.POST("/refresh", jwtStrategy.RefreshHandler)
In the above code, we create a variable of type auth.JWTStrategy
through the function newJWTAuth
, which includes some authentication-related functions.
- LoginHandler: Implements Basic authentication for login verification.
- RefreshHandler: Refreshes the expiration time of the token.
- LogoutHandler: Called when a user logs out. If the authentication-related information is set in the cookie after a successful login, executing the LogoutHandler will clear this information.
Next, let’s discuss LoginHandler, RefreshHandler, and LogoutHandler separately.
- LoginHandler
Now, let’s take a look at the LoginHandler Gin middleware function, which is defined in the auth_jwt.go file of the github.com/appleboy/gin-jwt
package.
func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
if mw.Authenticator == nil {
mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrMissingAuthenticatorFunc, c))
return
}
data, err := mw.Authenticator(c)
if err != nil {
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
return
}
// Create the token
token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
claims := token.Claims.(jwt.MapClaims)
if mw.PayloadFunc != nil {
for key, value := range mw.PayloadFunc(data) {
claims[key] = value
}
}
expire := mw.TimeFunc().Add(mw.Timeout)
claims["exp"] = expire.Unix()
claims["orig_iat"] = mw.TimeFunc().Unix()
tokenString, err := mw.signedString(token)
if err != nil {
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
return
}
// set cookie
if mw.SendCookie {
expireCookie := mw.TimeFunc().Add(mw.CookieMaxAge)
maxage := int(expireCookie.Unix() - mw.TimeFunc().Unix())
if mw.CookieSameSite != 0 {
c.SetSameSite(mw.CookieSameSite)
}
c.SetCookie(
mw.CookieName,
tokenString,
maxage,
"/",
mw.CookieDomain,
mw.SecureCookie,
mw.CookieHTTPOnly,
)
}
mw.LoginResponse(c, http.StatusOK, tokenString, expire)
}
From the implementation of the LoginHandler function, we can see that it executes the Authenticator
function to perform Basic authentication. If the authentication is successful, a JWT token is issued, and the PayloadFunc
function is executed to set the token payload. If we set SendCookie=true
, it will also add authentication-related information, such as the token, its lifecycle, etc., to the cookie. Finally, it executes the LoginResponse
method to return the token and its expiration time.
The Authenticator
, PayloadFunc
, and LoginResponse
functions are specified when creating the JWT authentication strategy. Next, let’s discuss each of them separately.
First, let’s look at the authenticator
function. The Authenticator
function retrieves the username and password from the HTTP Authorization header and validates whether the password is valid.
func authenticator() func(c *gin.Context) (interface{}, error) {
return func(c *gin.Context) (interface{}, error) {
var login loginInfo
var err error
// support header and body both
if c.Request.Header.Get("Authorization") != "" {
login, err = parseWithHeader(c)
} else {
login, err = parseWithBody(c)
}
if err != nil {
return "", jwt.ErrFailedAuthentication
}
// Get the user information by the login username.
user, err := store.Client().Users().Get(c, login.Username, metav1.GetOptions{})
if err != nil {
log.Errorf("get user information failed: %s", err.Error())
return "", jwt.ErrFailedAuthentication
}
// Compare the login password with the user password.
The Authenticator
function retrieves the username and password either from the header or the request body, and then calls the parseWithHeader
or parseWithBody
function to parse them. If an error occurs during parsing, it returns jwt.ErrFailedAuthentication
. It then obtains the user information based on the login username and compares the login password with the user password.
if err := user.Compare(login.Password); err != nil {
return "", jwt.ErrFailedAuthentication
}
return user, nil
The Authenticator
function needs to get the username and password. It first checks if there is an Authorization
request header. If there is, it calls the parseWithHeader
function to get the username and password. Otherwise, it calls parseWithBody
to get the username and password from the body. If both methods fail, it returns an authentication failure error.
Therefore, the Basic support of the IAM project includes the following two request methods:
$ curl -XPOST -H"Authorization: Basic YWRtaW46QWRtaW5AMjAyMQ==" http://127.0.0.1:8080/login # The username and password are encoded with base64 and passed through the HTTP Authorization Header. It is recommended to use this method because the password is not in plain text.
$ curl -s -XPOST -H'Content-Type: application/json' -d'{"username":"admin","password":"Admin@2021"}' http://127.0.0.1:8080/login # The username and password are passed in the HTTP Body. Since the password is in plain text, this method is not recommended for actual development.
Now let’s take a look at how the parseWithHeader
function gets the username and password. Let’s assume our request is:
$ curl -XPOST -H"Authorization: Basic YWRtaW46QWRtaW5AMjAyMQ==" http://127.0.0.1:8080/login
Where YWRtaW46QWRtaW5AMjAyMQ==
is generated by the following command:
$ echo -n 'admin:Admin@2021' | base64
YWRtaW46QWRtaW5AMjAyMQ==
The parseWithHeader
function actually performs the reverse steps of the above command:
- Get the value of the
Authorization
header and call thestrings.SplitN
function to get a slice variableauth
with the value["Basic","YWRtaW46QWRtaW5AMjAyMQ=="]
. - Decode
YWRtaW46QWRtaW5AMjAyMQ==
with base64 and getadmin:Admin@2021
. - Call the
strings.SplitN
function to getadmin:Admin@2021
, and then get the usernameadmin
and the passwordAdmin@2021
.
The parseWithBody
function calls the ShouldBindJSON
function of Gin to parse the username and password from the body.
After obtaining the username and password, the program queries the encrypted password corresponding to the user from the database. Let’s assume it is xxxx
. Finally, the authenticator
function calls user.Compare
to check if xxxx
matches the encrypted string generated by user.Compare
. If it matches, the authentication succeeds; otherwise, it returns an authentication failure.
Now let’s take a look at the PayloadFunc
function:
func payloadFunc() func(data interface{}) jwt.MapClaims {
return func(data interface{}) jwt.MapClaims {
claims := jwt.MapClaims{
"iss": APIServerIssuer,
"aud": APIServerAudience,
}
if u, ok := data.(*v1.User); ok {
claims[jwt.IdentityKey] = u.Name
claims["sub"] = u.Name
}
return claims
}
}
The PayloadFunc
function sets the iss
, aud
, sub
, and identity
fields of the payload in the JWT Token for later use.
Next, let’s look at the third function we mentioned, LoginResponse
:
func loginResponse() func(c *gin.Context, code int, token string, expire time.Time) {
return func(c *gin.Context, code int, token string, expire time.Time) {
c.JSON(http.StatusOK, gin.H{
"token": token,
"expire": expire.Format(time.RFC3339),
})
}
}
This function is used to return the token and its expiration time to the caller after successful Basic authentication:
$ curl -XPOST -H"Authorization: Basic YWRtaW46QWRtaW5AMjAyMQ==" http://127.0.0.1:8080/login
{"expire":"2021-09-29T01:38:49+08:00","token":"XX.YY.ZZ"}
After successful login, the iam-apiserver will return the token and its expiration time. The frontend can store this information in cookies or localStorage, and use the token for authentication in subsequent requests. Using a token for authentication not only improves authentication security, but also avoids database queries, thereby improving authentication efficiency.
- RefreshHandler
The RefreshHandler
function first performs Bearer authentication. If the authentication passes, it will reissue the token.
- LogoutHandler
Finally, let’s take a look at the LogoutHandler
function:
func (mw *GinJWTMiddleware) LogoutHandler(c *gin.Context) {
// delete auth cookie
if mw.SendCookie {
if mw.CookieSameSite != 0 {
c.SetSameSite(mw.CookieSameSite)
}
c.SetCookie(
mw.CookieName,
"",
-1,
"/",
mw.CookieDomain,
mw.SecureCookie,
mw.CookieHTTPOnly,
)
}
mw.LogoutResponse(c, http.StatusOK)
}
As you can see, the LogoutHandler
is used to clear the Bearer authentication-related information in the cookie.
In summary, Basic authentication uses the username and password for authentication and is usually used in the login interface /login
. After the user logs in successfully, a JWT token is returned, which is then saved in the browser’s cookie or localStorage for subsequent requests.
For subsequent requests, the token is always included to complete the Bearer authentication. In addition, there are usually corresponding /logout
and /refresh
interfaces for logout and token refreshing.
You may wonder why token refreshing is needed. This is because the token issued by the login interface has an expiration time. With the refresh interface, the frontend can refresh the expiration time of the token according to its needs. The expiration time can be specified by the jwt.timeout
configuration item in the iam-apiserver configuration file. The secret key (secretKey) used when issuing the token during login is specified by the jwt.key
configuration item.
How is Bearer authentication implemented in the IAM project? #
Earlier, we introduced Basic authentication. Here, I will introduce how Bearer authentication is implemented in the IAM project.
There are two parts in the IAM project that implement Bearer authentication: iam-apiserver and iam-authz-server. Let me explain how they each implement Bearer authentication.
Implementation of Bearer authentication in iam-authz-server #
Let’s start by looking at how iam-authz-server implements Bearer authentication.
iam-authz-server uses the cache authentication middleware by loading it in the /v1
route group:
auth := newCacheAuth()
apiv1 := g.Group("/v1", auth.AuthFunc())
Let’s take a look at the newCacheAuth
function:
func newCacheAuth() middleware.AuthStrategy {
return auth.NewCacheStrategy(getSecretFunc())
}
func getSecretFunc() func(string) (auth.Secret, error) {
return func(kid string) (auth.Secret, error) {
cli, err := store.GetStoreInsOr(nil)
if err != nil {
return auth.Secret{}, errors.Wrap(err, "get store instance failed")
}
secret, err := cli.GetSecret(kid)
if err != nil {
return auth.Secret{}, err
}
return auth.Secret{
Username: secret.Username,
ID: secret.SecretId,
Key: secret.SecretKey,
Expires: secret.Expires,
}, nil
}
}
The newCacheAuth
function calls auth.NewCacheStrategy
to create a cache authentication strategy, passing the getSecretFunc
function as a parameter. The getSecretFunc
function returns the secret key information. The secret key information contains the following fields:
type Secret struct {
Username string
ID string
Key string
Expires int64
}
Now let’s take a look at the AuthFunc
method implemented by the cache authentication strategy: source
func (cache CacheStrategy) AuthFunc() gin.HandlerFunc {
return func(c *gin.Context) {
header := c.Request.Header.Get("Authorization")
if len(header) == 0 {
core.WriteResponse(c, errors.WithCode(code.ErrMissingHeader, "Authorization header cannot be empty."), nil)
c.Abort()
return
}
var rawJWT string
// Parse the header to get the token part.
fmt.Sscanf(header, "Bearer %s", &rawJWT)
// Use own validation logic, see below
var secret Secret
claims := &jwt.MapClaims{}
// Verify the token
parsedT, err := jwt.ParseWithClaims(rawJWT, claims, func(token *jwt.Token) (interface{}, error) {
// Validate the alg is HMAC signature
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, ErrMissingKID
}
var err error
secret, err = cache.get(kid)
if err != nil {
return nil, ErrMissingSecret
}
return []byte(secret.Key), nil
}, jwt.WithAudience(AuthzAudience))
if err != nil || !parsedT.Valid {
core.WriteResponse(c, errors.WithCode(code.ErrSignatureInvalid, err.Error()), nil)
c.Abort()
return
}
if KeyExpired(secret.Expires) {
tm := time.Unix(secret.Expires, 0).Format("2006-01-02 15:04:05")
core.WriteResponse(c, errors.WithCode(code.ErrExpired, "expired at: %s", tm), nil)
c.Abort()
return
}
c.Set(CtxUsername, secret.Username)
c.Next()
}
}
// KeyExpired checks if a key has expired, if the value of user.SessionState.Expires is 0, it will be ignored.
func KeyExpired(expires int64) bool {
if expires >= 1 {
return time.Now().After(time.Unix(expires, 0))
}
return false
}
The AuthFunc
function completes the JWT authentication in four steps, each step consisting of several sub-steps. Let’s go through them together.
Step 1: Retrieve XX.YY.ZZ from the “Authorization: Bearer XX.YY.ZZ” request header. XX.YY.ZZ represents the JWT Token.
Step 2: Call the ParseWithClaims
function provided by the github.com/dgrijalva/jwt-go
package, which performs the following four steps:
-
Call the
ParseUnverified
function, which performs the following operations:- Retrieve the first part XX from the token, decode it using base64, and obtain the JWT Token’s Header
{"alg":"HS256","kid":"a45yPqUnQ8gljH43jAGQdRo0bXzNLjlU0hxa","typ":"JWT"}
. - Retrieve the second part YY from the token, decode it using base64, and obtain the JWT Token’s Payload
{"aud":"iam.authz.marmotedu.com","exp":1625104314,"iat":1625097114,"iss":"iamctl","nbf":1625097114}
. - Retrieve the token’s encryption function based on the
alg
field in the token header. - The
ParseUnverified
function will ultimately return a token variable of type*jwt.Token
. This token contains important fields such as Method, Header, Claims, and Valid, which are used in the subsequent authentication steps.
- Retrieve the first part XX from the token, decode it using base64, and obtain the JWT Token’s Header
-
Call the
keyFunc
parameter to retrieve the secret key. Here’s an implementation of thekeyFunc
:
func(token *jwt.Token) (interface{}, error) {
// Validate the alg is HMAC signature
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, ErrMissingKID
}
var err error
secret, err = cache.get(kid)
if err != nil {
return nil, ErrMissingSecret
}
return []byte(secret.Key), nil
}
You can see that keyFunc
receives a variable of type *Token
and retrieves the kid from the Token Header, where kid is the key ID: secretID. Then, it calls cache.get(kid)
to retrieve the secretKey. The cache.get
function is actually getSecretFunc
, which searches for the key information in memory based on the kid. The key information contains the secretKey.
- To obtain the Signature string ZZ from the Token, which is the third segment of the Token.
- After obtaining the secretKey,
token.Method.Verify
verifies if the Signature string ZZ, which is the third segment of the Token, is valid.token.Method.Verify
actually uses the same encryption algorithm and the same secretKey to encrypt the string XX.YY. Let’s assume the encrypted string is WW. Next, WW is compared with the base64-decoded string of ZZ. If they are equal, the authentication is passed. Otherwise, the authentication fails.
Step 3: Call KeyExpired
to verify if the secret has expired. The secret information includes the expiration time, and you only need to compare it with the current time.
Step 4: Set the HTTP Header username: colin
.
With this, the analysis of the Bearer authentication process in iam-authz-server
is complete.
Let’s summarize: iam-authz-server
applies Bearer authentication by loading it as a Gin middleware when accessing the /v1/authz
interface. Bearer authentication has the advantages of an expiration time and the ability to carry more useful information in the authentication string. It also provides irreversible encryption. Therefore, Bearer authentication and the JWT format for Tokens, which are the most popular authentication method in the industry for API authentication, are adopted for /v1/authz
.
Bearer authentication requires secretID and secretKey, which are obtained through gRPC API calls from iam-apisaerver
and cached in memory in iam-authz-server
for authentication queries.
When a request arrives, the Bearer authentication middleware in iam-authz-server
parses the Header from the JWT Token and retrieves the secretID from the kid field of the Header. Then, it searches for the secretKey based on the secretID and encrypts the Header and Payload of the JWT Token using the secretKey. Finally, it compares the result with the Signature section. If they match, the authentication is passed; otherwise, the authentication fails.
Implementation of Bearer Authentication in iam-apiserver #
Now let’s look at the Bearer authentication in iam-apiserver
.
Bearer authentication in iam-apiserver
is specified by the following code (located in the router.go
file at line 65):
v1.Use(auto.AuthFunc())
Let’s take a look at the implementation of auto.AuthFunc()
in auto.go:
func (a AutoStrategy) AuthFunc() gin.HandlerFunc {
return func(c *gin.Context) {
operator := middleware.AuthOperator{}
authHeader := strings.SplitN(c.Request.Header.Get("Authorization"), " ", 2)
if len(authHeader) != authHeaderCount {
core.WriteResponse(
c,
errors.WithCode(code.ErrInvalidAuthHeader, "Authorization header format is wrong."),
nil,
)
c.Abort()
return
}
switch authHeader[0] {
case "Basic":
operator.SetStrategy(a.basic)
case "Bearer":
operator.SetStrategy(a.jwt)
// a.JWT.MiddlewareFunc()(c)
default:
core.WriteResponse(c, errors.WithCode(code.ErrSignatureInvalid, "unrecognized Authorization header."), nil)
c.Abort()
return
}
operator.AuthFunc()(c)
c.Next()
}
}
From the above code, we can see that the AuthFunc
function parses the authentication method (either Basic or Bearer) from the Authorization Header. If it is Bearer, it uses the JWT authentication strategy; if it is Basic, it uses the Basic authentication strategy.
Next, let’s look at the implementation of the AuthFunc
function for the JWT strategy in jwt.go:
func (j JWTStrategy) AuthFunc() gin.HandlerFunc {
return j.MiddlewareFunc()
}
By following the code, we can find that the MiddlewareFunc
function eventually calls the middlewareImpl
method of the GinJWTMiddleware
struct from the github.com/appleboy/gin-jwt
package auth_jwt.go:
func (mw *GinJWTMiddleware) middlewareImpl(c *gin.Context) {
claims, err := mw.GetClaimsFromJWT(c)
if err != nil {
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
return
}
if claims["exp"] == nil {
mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c))
return
}
if _, ok := claims["exp"].(float64); !ok {
mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c))
return
}
if int64(claims["exp"].(float64)) < mw.TimeFunc().Unix() {
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
return
}
c.Set("JWT_PAYLOAD", claims)
identity := mw.IdentityHandler(c)
if identity != nil {
c.Set(mw.IdentityKey, identity)
}
if !mw.Authorizator(identity, c) {
mw.unauthorized(c, http.StatusForbidden, mw.HTTPStatusMessageFunc(ErrForbidden, c))
return
}
c.Next()
}
Analyzing the above code, we can understand the Bearer authentication process of middlewareImpl
:
Step 1: Calls the GetClaimsFromJWT
function to retrieve the Authorization Header from the HTTP request, parse the Token string, and perform authentication. Finally, it returns the Token Payload.
Step 2: Validates if the exp
in the Payload has exceeded the current time. If it has, the Token has expired and the validation fails.
Step 3: Adds the JWT_PAYLOAD
key to the gin.Context
for later use in subsequent code (although it might not be necessary).
Step 4: Adds the IdentityKey key to the gin.Context
through the following code. The IdentityKey key can be specified when creating the GinJWTMiddleware
struct. In this case, it is set to middleware.UsernameKey
, which is the username.
identity := mw.IdentityHandler(c)
if identity != nil {
c.Set(mw.IdentityKey, identity)
}
The value of the IdentityKey key is returned by the IdentityHandler function:
func(c *gin.Context) interface{} {
claims := jwt.ExtractClaims(c)
return claims[jwt.IdentityKey]
}
The above function retrieves the value of the identity field from the Token’s Payload. The value of the identity field is actually the username, and you can check the payloadFunc function for more information.
Step 5: Calls the Authorizator
method. Authorizator
is a callback function that must return true for success and false for failure. Authorizator
is also specified when creating the GinJWTMiddleware
. For example:
func authorizator() func(data interface{}, c *gin.Context) bool {
return func(data interface{}, c *gin.Context) bool {
if v, ok := data.(string); ok {
log.L(c).Infof("user `%s` is authenticated.", v)
return true
}
return false
}
}
The authorizator
function returns an anonymous function. When authentication is successful, the anonymous function logs a successful authentication message.
Tips for Designing Authentication Functionality in IAM Projects #
When designing the authentication functionality in an IAM project, I also employed some techniques, which I would like to share with you here.
Tip 1: Interface-Oriented Programming #
When using the NewAutoStrategy
function to create an auto authentication strategy, a parameter of type middleware.AuthStrategy
interface is passed, which means that different implementations of Basic and Bearer authentication can be used, allowing for the extension of new authentication methods as needed.
Tip 2: Using Abstract Factory Pattern #
In the auth.go file, when creating authentication strategies using newBasicAuth
, newJWTAuth
, and newAutoAuth
, interfaces are returned. By returning interfaces, various authentication functionalities provided by you can be used by callers without exposing the internal implementation.
Tip 3: Using Strategy Pattern #
In the auto authentication strategy, we choose and set the authentication strategy (Basic or Bearer) based on the Authorization: XXX X.Y.X
HTTP request header. You can check the AuthFunc
function of AutoStrategy
for details:
func (a AutoStrategy) AuthFunc() gin.HandlerFunc {
return func(c *gin.Context) {
operator := middleware.AuthOperator{}
authHeader := strings.SplitN(c.Request.Header.Get("Authorization"), " ", 2)
...
switch authHeader[0] {
case "Basic":
operator.SetStrategy(a.basic)
case "Bearer":
operator.SetStrategy(a.jwt)
// a.JWT.MiddlewareFunc()(c)
default:
core.WriteResponse(c, errors.WithCode(code.ErrSignatureInvalid, "unrecognized Authorization header."), nil)
c.Abort()
return
}
operator.AuthFunc()(c)
c.Next()
}
}
In the above code, if it is Basic, it is set as the Basic authentication method operator.SetStrategy(a.basic)
; if it is Bearer, it is set as the Bearer authentication method operator.SetStrategy(a.jwt)
. The SetStrategy
method takes a parameter of type AuthStrategy
interface, which implements the AuthFunc() gin.HandlerFunc
function for authentication. Therefore, we can complete the authentication by calling operator.AuthFunc()(c)
.
Summary #
In the IAM project, iam-apiserver implements Basic authentication and Bearer authentication, while iam-authz-server implements Bearer authentication. This lecture focuses on the authentication implementation of iam-apiserver.
To access iam-apiserver, users need to first pass Basic authentication. Once authenticated, a JWT Token and its expiration time will be returned. The front-end stores the Token in LocalStorage or Cookie, and all subsequent requests are authenticated using this Token.
During Basic authentication, iam-apiserver parses the username and password from the HTTP Authorization Header, encrypts the password again, and compares it with the saved value in the database. If they do not match, the authentication fails; otherwise, it succeeds. After successful authentication, a Token is returned, with the username set in the Payload section using “username” as the key.
During Bearer authentication, iam-apiserver parses the Header and Payload from the JWT Token and retrieves the encryption algorithm from the Header. Then, it encrypts Header.Payload again using the obtained encryption algorithm and the secret key obtained from the configuration file to obtain the Signature. The two Signatures are compared. If they do not match, an HTTP 401 Unauthorized error is returned. If they match, the Token’s expiration is checked. If it has expired, authentication is not passed; otherwise, it is passed. After successful authentication, the username from the Payload is added to a gin.Context variable for subsequent business logic to use.
I have created a diagram illustrating the entire process, which you can refer to for review.
Exercise #
- Read the
GetClaimsFromJWT
method of theGinJWTMiddleware
structure in thegithub.com/appleboy/gin-jwt
package. Analyze how theGetClaimsFromJWT
method extracts and authenticates the token from thegin.Context
. - Consider whether
iam-apiserver
andiam-authzserver
can use the same authentication strategy. If so, how can it be implemented?
Feel free to leave a comment in the section below to discuss and exchange ideas. See you in the next lesson.