<p>I've spent a few hours now looking at pretty much every link I can find about setting up a simple JWT-based auth system in my web application, but I'm not quite at the finish line yet. It looks like the main JWT package has been updated since a few of the videos/tutorials on using them have been made, so I'm missing a few pieces of the puzzle that I'm hoping someone can help fill in.</p>
<p>Here's the function I'm using to generate a JWT when someone hits the /login endpoint: </p>
<pre><code>func LoginHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
decoder := json.NewDecoder(r.Body)
var u User
err := decoder.Decode(&u)
if err != nil {
panic(err)
}
defer r.Body.Close()
email := u.Email
password := u.Password
// Check database here
if strings.ToLower(email) != "testEmail.com" {
if password != "pw" {
w.WriteHeader(http.StatusForbidden)
fmt.Println("Error logging in")
fmt.Fprint(w, "Invalid credentials")
return
}
}
signer := jwt.New(jwt.GetSigningMethod("RS256"))
// Dealing with claims
claims := make(jwt.MapClaims)
claims["iss"] = "testClaim"
claims["exp"] = time.Now().Add(time.Minute * 10).Unix()
claims["CustomUserInfo"] = struct {
Email string
Role string
}{email, "user"}
signer.Claims = claims
parsedPrivateKey, err := jwt.ParseRSAPrivateKeyFromPEM(SignKey)
if err != nil {
panic(err)
}
tokenString, err := signer.SignedString(parsedPrivateKey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Error while signing the token")
log.Printf("Error signing token: %v\n", err)
}
response := Token{tokenString}
JsonResponse(response, w)
// Just to see the response
fmt.Printf("%+v", response)
}
</code></pre>
<p>JsonResponse just writes json to the header. My first question is more generally about JWTs: when someone hits the /login endpoint, their credentials checked by the database, and a JWT is generated (per the function above), where is the JWT stored? In this case, wouldn't it only be stored on the header for that specific response? Should I be storing this JWT in cookies, or local storage? </p>
<p>Finally (and the most crucial piece I'm missing), how should I go about validating a token? The ParseFromRequest function seems to have been removed from the JWT package. </p>
<p>I started with an outline like this: </p>
<pre><code>func ValidateToken(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return rsaSecret, nil
})
</code></pre>
<p>Is this the right direction? Should I be checking the request header for the "Authorization bearer {token}"? as the tokenString value? Sorry for the long post, any help at all would be greatly appreciated!</p>
<hr/>**评论:**<br/><br/>earthboundkid: <pre><p>I don't know much about JWT, but this is worth saying in case anyone is new to web dev:</p>
<p>The line <code>password != "pw"</code> should not be translated into working code (checking that the password is in the DB). Passwords should not be stored in databases because if they are and a hacker steals your database (for example by getting a database backup from a rogue worker or stealing a laptop), then they will know all your users passwords and be able to try those same username+password combinations on other sites, to check for password reuse.</p>
<p>Instead, what you should store in the database is a "salted hash" of the password. Essentially, with a salted hash, a user's password is turned into an opaque string that cannot be reversed back into the plain text. The most popular algorithm for this is bcrypt. There are more details, but that's the minimum anyone dealing with passwords absolutely has to know. Google around and you should be able to find more about constant time comparisons and whatnot.</p></pre>ConfuciusBateman: <pre><p>Oh I definitely would not use this in production. This was just to test that my HTTP requests were working correctly.</p></pre>earthboundkid: <pre><p>I figured as much, just thought it was better to be safe than sorry. :-)</p></pre>ConfuciusBateman: <pre><p>Haha no worries, I appreciate the lookout! </p></pre>Tikiatua: <pre><p>I am currently developing a single sign on solution for our various services and did some extensive research on best practice recommendations for jwts.</p>
<p>Some good resources were the documentation and blog posts of auth0 (<a href="https://auth0.com" rel="nofollow">https://auth0.com</a>) and stormpath (<a href="https://stormpath.com" rel="nofollow">https://stormpath.com</a>) as well as the specification of OpenID connect (<a href="http://openid.net/connect/" rel="nofollow">http://openid.net/connect/</a>).</p>
<p>That said, best practice really boil down to your security requirements and to what you want to use jwts for. </p>
<p>The basic problem with jwts is, that someone might steel the token and would then be able to login as the user until the jwt expires. </p>
<p>Then there is sort of a federation problem: Let's say you have a single-sign-on (like for google) that should allow your users to access multiple services after they have logged in. If you are using oauth, the service would get the user authenticated from single-sign-on and then establish its own session by - for example - using jwts. Now the problem is, that jwts typically only get saved on the client. Hence, it becomes quite tricky to stop the session for all services once the users is login out from the single-sign on. This would require to somehow delete the jwts from all services on the client or require you to have a blacklist on the server - which sort of brings you back to the regular session management before jwts. There are some workarounds, but it does get rather complicated.</p>
<p>It becomes simpler, if we focus on a single service. Were to store, respectively check for the jwt basically boils down to three options:</p>
<ul>
<li><p>Request header with "Authentication bearer {token}". Really convenient for api calls from the command line. Question would be were the client would save the token</p></li>
<li><p>Saving the jwt in local storage - as done by auth0. This gives your frontend code access to the information stored in the jwt. Which is great for example for single page applications that should adapt based on user permissions or show information about the current user. However, there is a certain attach surface for this, as the jwt is accessible with javascript. In addition you must implement the functionality to pass the jwt to the server on every request (i.e. as post parameter or as header token)</p></li>
<li><p>Saving the jwt in a cookie - recommended in a blog post from stormpath. This is really convenient, as we can leverage the browser functionality to store and expire jwt-cookies. For security reasons the cookies should be set to httpOnly (no javascript access) and secure (to only be passed over https)</p></li>
</ul>
<p>Here is the code I am using for a web-application that uses cookies to store the jwt (only example code).</p>
<pre><code>jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/pkg/errors"
"github.com/uber-go/zap"
// should be replaced by your custom secret
var tokenSecret = []byte("abcdefgh")
const tokenIssuer = "your.domain"
// define custom token claims
type tokenClaims struct {
models.User
jwt.StandardClaims
}
func (env *Environment) requireAuthentication(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
// check if the users has a jwt token set in the cookie
cookie, err := ctx.Cookie(tokenIssuer)
if err != nil {
env.Logger.Info("could not read token cookie", zap.Error(err))
return ctx.Render(http.StatusOK, "login.html", nil)
}
// parse the token string from the cookie
tokenString := cookie.Value
// check if the token is authenticated
token, err := jwt.ParseWithClaims(tokenString, &tokenClaims{}, func(token *jwt.Token) (interface{}, error) {
// validate that the signig method is correct
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return tokenSecret, nil
})
if err != nil {
env.Logger.Error("jwt token error", zap.Error(err))
return unauthorizedResponse(ctx)
}
// try to extract our custom claims from the token
claims, ok := token.Claims.(*tokenClaims)
// check if the token is valid
if !ok || token.Valid == false {
env.Logger.Info("token is expired", zap.Int64("expired", claims.ExpiresAt))
return unauthorizedResponse(ctx)
}
// save the user information in the context
ctx.Set("user", claims.User)
// issue a new cookie for the token
err = generateAuthToken(ctx, claims.User)
if err != nil {
env.Logger.Error("could not generate new token", zap.Error(err))
}
// handle the next function in the chain
return next(ctx)
}
}
// generateAuthToken will issue a new authentication token
func generateAuthToken(ctx echo.Context, user models.User) error {
// define the claims for the jwt token
claims := tokenClaims{
user,
jwt.StandardClaims{
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
Issuer: tokenIssuer,
},
}
// create a new token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// sign the token with our secret
tokenString, err := token.SignedString(tokenSecret)
if err != nil {
return errors.Wrap(err, "could not generate the jwt token")
}
cookie := http.Cookie{}
// set the name and value of our token cookie
cookie.Name = tokenIssuer
cookie.Value = tokenString
// expire cookie in an hour
cookie.Expires = time.Now().Add(1 * time.Hour)
// cookie shall not be accessible by javascript
cookie.HttpOnly = true
// always require https connections
cookie.Secure = true
// write the cookie header to the given context
ctx.SetCookie(&cookie)
return nil
}
func setExpiredCookie(ctx echo.Context) {
cookie := http.Cookie{}
// set the name and value of our token cookie
cookie.Name = tokenIssuer
cookie.Value = ""
// expire cookie in an hour
cookie.Expires = time.Now().Add(-2 * time.Hour)
// cookie shall not be accessible by javascript
cookie.HttpOnly = true
// always require https connections
cookie.Secure = true
// write the cookie header to the given context
ctx.SetCookie(&cookie)
}
</code></pre></pre>luckyleprechaun98: <pre><p>Typically you put the JWT in the Auth header as <code>Bearer <token></code> but you can also pass it as a cookie or as a URL query parameter. </p>
<p>I wrote the JWT middleware for Caddy server. Feel free to check it out for some ideas. </p>
<p><a href="https://github.com/BTBurke/caddy-jwt" rel="nofollow">https://github.com/BTBurke/caddy-jwt</a></p></pre>Asti_: <pre><p>I would love to see a tutorial from someone on how to implement JWTs from scratch, explaining benefits and reasons of choices that should be considered when implementing them. </p></pre>TheMue: <pre><p>Dunno exactly which package you've chosen, but I developed my own one at <a href="https://godoc.org/github.com/tideland/gorest/jwt" rel="nofollow">https://godoc.org/github.com/tideland/gorest/jwt</a>. The unit tests at <a href="https://github.com/tideland/gorest/tree/master/jwt" rel="nofollow">https://github.com/tideland/gorest/tree/master/jwt</a> show how to use it.</p></pre>thewhitetulip: <pre><p>I invite you to take a look at this chapter: <a href="https://github.com/thewhitetulip/web-dev-golang-anti-textbook/blob/master/manuscript/8.0buildingAPI.md" rel="nofollow">https://github.com/thewhitetulip/web-dev-golang-anti-textbook/blob/master/manuscript/8.0buildingAPI.md</a></p>
<p>Also at <a href="https://github.com/thewhitetulip/Tasks/" rel="nofollow">https://github.com/thewhitetulip/Tasks/</a>, I have implemented JWT</p></pre>
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码`
- 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传