Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to build an instant messaging application by OAuth

2025-05-03 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/02 Report--

Most people do not understand the knowledge points of this article "how to build an instant messaging application with OAuth", so the editor summarizes the following content, detailed content, clear steps, and has a certain reference value. I hope you can get something after reading this article. Let's take a look at this "how to build an instant messaging application with OAuth" article.

How social login works is simple: the user clicks the link and redirects to the GitHub authorization page. When the user gives us access to his personal information, it will be redirected back to the login page. The next time we try to log in, the system will not ask for authorization again, that is, our application has already remembered this user. This makes the whole login process seem as fast as you click with your mouse.

If you further consider its internal implementation, the process will become more complex. First, we need to register a new GitHub OAuth application.

In this step, the more important thing is to call back URL. We set it to http://localhost:3000/api/oauth/github/callback. This is because we always work on the local host during the development process. Once you want to deliver the application to production, please register a new application with the correct callback URL.

After registering, you will receive a "client id" and a "security key". To be on the safe side, please don't share them with anyone?

By the way, let's start writing some code. Now, create a main.go file:

Package main import ("database/sql", "fmt", "log", "net/http", "net/url", "os", "strconv", "github.com/gorilla/securecookie", "github.com/joho/godotenv", "github.com/knq/jwt" _ "github.com/lib/pq", "github.com/matryer/way", "golang.org/x/oauth3"golang.org" / x/oauth3/github ") var origin * url.URLvar db * sql.DBvar githubOAuthConfig * oauth3.Configvar cookieSigner * securecookie.SecureCookievar jwtSigner jwt.Signer func main () {godotenv.Load () port: = intEnv (" PORT ") 3000) originString: = env ("ORIGIN", fmt.Sprintf ("http://localhost:%d/", port)) databaseURL: = env (" DATABASE_URL "," postgresql://root@127.0.0.1:26257/messenger?sslmode=disable ") githubClientID: = os.Getenv (" GITHUB_CLIENT_ID ") githubClientSecret: = os.Getenv (" GITHUB_CLIENT_SECRET ") hashKey: = env (" HASH_KEY ") "secret") jwtKey: = env ("JWT_KEY", "secret") var err error if origin, err = url.Parse (originString) Err! = nil | |! origin.IsAbs () {log.Fatal ("invalid origin") return} if I, err: = strconv.Atoi (origin.Port ()) Err = = nil {port = I} if githubClientID = "" | githubClientSecret = = "" {log.Fatalf ("remember to set both $GITHUB_CLIENT_ID and $GITHUB_CLIENT_SECRET") return} if db, err = sql.Open ("postgres", databaseURL) Err! = nil {log.Fatalf ("could not open database connection:% v\ n", err) return} defer db.Close () if err = db.Ping () Err! = nil {log.Fatalf ("could not ping to db:% v\ n", err) return} githubRedirectURL: = * origin githubRedirectURL.Path = "/ api/oauth/github/callback" githubOAuthConfig = & oauth3.Config {ClientID: githubClientID, ClientSecret: githubClientSecret, Endpoint: github.Endpoint, RedirectURL: githubRedirectURL.String (), Scopes: [] string {"read:user"} } cookieSigner = securecookie.New ([] byte (hashKey), nil) .MaxAge (0) jwtSigner, err = jwt.HS256.New ([] byte (jwtKey)) if err! = nil {log.Fatalf ("could not create JWT signer:% v\ n", err) return} router: = way.NewRouter () router.HandleFunc ("GET", "/ api/oauth/github", githubOAuthStart) router.HandleFunc ("GET") "/ api/oauth/github/callback", githubOAuthCallback) router.HandleFunc ("GET", "/ api/auth_user", guard (getAuthUser)) log.Printf ("accepting connections on port% d\ n", port) log.Printf ("starting server at% s\ n", origin.String () addr: = fmt.Sprintf (":% d", port) if err = http.ListenAndServe (addr, router) Err! = nil {log.Fatalf ("could not start server:% v\ n", err)}} func env (key, fallbackValue string) string {v, ok: = os.LookupEnv (key) if! ok {return fallbackValue} return v} func intEnv (key string, fallbackValue int) int {v, ok: = os.LookupEnv (key) if! ok {return fallbackValue} I Err: = strconv.Atoi (v) if err! = nil {return fallbackValue} return I}

Install dependencies:

Go get-u github.com/gorilla/securecookiego get-u github.com/joho/godotenvgo get-u github.com/knq/jwtgo get-u github.com/lib/pqge get-u github.com/matoous/go-nanoidgo get-u github.com/matryer/waygo get-u golang.org/x/oauth3

We will use the .env file to save keys and other configurations. Please create this file and make sure it contains at least the following:

GITHUB_CLIENT_ID=your_github_client_idGITHUB_CLIENT_SECRET=your_github_client_secret

Other environment variables we will also use are:

PORT: the port on which the server is running. The default value is 3000.

ORIGIN: your domain name. The default value is http://localhost:3000/. We can also specify the port here.

Address of the DATABASE_URL:Cockroach database. The default value is postgresql://root@127.0.0.1:26257/messenger?sslmode=disable.

HASH_KEY: the key used to sign the cookie. Yes, we will use signed cookie to ensure security.

JWT_KEY: used to sign JSON

Network token Web Token

The key of the.

Because the default values are already set in the code, you don't have to write them to the .env file.

After reading the configuration and connecting to the database, we will create an OAuth configuration. We will use the ORIGIN information to build the callback URL (just like the one we registered on the GitHub page). Our data range is set to "read:user". This will allow us to read public user information, and all we need here is his user name and profile picture. Then we will initialize the cookie and JWT signers. Define some endpoints and start the server.

Before implementing the HTTP handler, let's write some functions to send the HTTP response.

Func respond (w http.ResponseWriter, v interface {}, statusCode int) {b, err: = json.Marshal (v) if err! = nil {respondError (w, fmt.Errorf ("could not marshal response:% v", err)) return} w.Header () .Set ("Content-Type", "application/json") Charset=utf-8 ") w.WriteHeader (statusCode) w.Write (b)} func respondError (w http.ResponseWriter, err error) {log.Println (err) http.Error (w, http.StatusText (http.StatusInternalServerError), http.StatusInternalServerError)}

The first function sends the JSON, while the second logs the error to the console and returns a 500 Internal Server Error error message.

OAuth start

So, the user clicks on the link that says "Access with GitHub". The link points to / api/oauth/github, which will redirect the user to github.

Func githubOAuthStart (w http.ResponseWriter, r * http.Request) {state, err: = gonanoid.Nanoid () if err! = nil {respondError (w, fmt.Errorf ("could not generte state:% v", err)) return} stateCookieValue, err: = cookieSigner.Encode ("state", state) if err! = nil {respondError (w, fmt.Errorf ("could not encode state cookie:% v") Err)) return} http.SetCookie (w, & http.Cookie {Name: "state", Value: stateCookieValue, Path: "/ api/oauth/github", HttpOnly: true,}) http.Redirect (w, r, githubOAuthConfig.AuthCodeURL (state), http.StatusTemporaryRedirect)}

OAuth3 uses a mechanism to prevent CSRF, so it needs a "state". We use Nanoid () to create a random string and use that string as the state. We also save it as a cookie.

OAuth callback

Once the user authorizes us to access his personal information, he will be redirected to this endpoint. The URL's query string will contain the status (state) and authorization code (code): / api/oauth/github/callback?state=&code=.

Const jwtLifetime = time.Hour * 24 * 14 type GithubUser struct {ID int `json: "id" `Login string `json: "login" `AvatarURL * string `json: "avatar_url,omitempty" `} type User struct {ID string `json: "id" `Username string `json: "username" `AvatarURL * string `json: "avatarUrl"`} func githubOAuthCallback (w http.ResponseWriter, r * http.Request) {stateCookie Err: = r.Cookie ("state") if err! = nil {http.Error (w, http.StatusText (http.StatusTeapot), http.StatusTeapot) return} http.SetCookie (w, & http.Cookie {Name: "state", Value: ", MaxAge:-1, HttpOnly: true,}) var state string if err = cookieSigner.Decode (" state ", stateCookie.Value) & state) Err! = nil {http.Error (w, http.StatusText (http.StatusTeapot), http.StatusTeapot) return} Q: = r.URL.Query () if state! = q.Get ("state") {http.Error (w, http.StatusText (http.StatusTeapot), http.StatusTeapot) return} ctx: = r.Context () t, err: = githubOAuthConfig.Exchange (ctx) Q.Get ("code") if err! = nil {respondError (w, fmt.Errorf ("could not fetch github token:% v", err)) return} client: = githubOAuthConfig.Client (ctx, t) resp, err: = client.Get ("https://api.github.com/user") if err! = nil {respondError (w, fmt.Errorf (" could not fetch github user:% v ") Err) return} var githubUser GithubUser if err = json.NewDecoder (resp.Body) .Decode (& githubUser) Err! = nil {respondError (w, fmt.Errorf ("could not decode github user:% v", err)) return} defer resp.Body.Close () tx, err: = db.BeginTx (ctx, nil) if err! = nil {respondError (w, fmt.Errorf ("could not begin tx:% v", err) return} var user User if err = tx.QueryRowContext (ctx, `SELECT id, username) Avatar_url FROM users WHERE github_id = $1`, githubUser.ID) .Scan (& user.ID, & user.Username, & user.AvatarURL) Err = = sql.ErrNoRows {if err = tx.QueryRowContext (ctx, `INSERT INTO users (username, avatar_url, github_id) VALUES ($1, $2, $3) RETURNING id `, githubUser.Login, githubUser.AvatarURL, githubUser.ID). Scan (& user.ID) Err! = nil {respondError (w, fmt.Errorf ("could not insert user:% v", err)) return} user.Username = githubUser.Login user.AvatarURL = githubUser.AvatarURL} else if err! = nil {respondError (w, fmt.Errorf ("could not query user by github ID:% v", err)) return} if err = tx.Commit () Err! = nil {respondError (w, fmt.Errorf ("could not commit to finish github oauth:% v", err)) return} exp: = time.Now () .Add (jwtLifetime) token, err: = jwtSigner.Encode (jwt.Claims {Subject: user.ID, Expiration: json.Number (strconv.FormatInt (exp.Unix (), 10),}) if err! = nil {respondError (w) Fmt.Errorf ("could not create token:% v", err) return} expiresAt, _: = exp.MarshalText () data: = make (url.Values) data.Set ("token", string (token)) data.Set ("expires_at", string (expiresAt)) http.Redirect (w, r, "/ callback?" + data.Encode (), http.StatusTemporaryRedirect)}

First, we will try to decode the cookie using the previously saved state. And compare it with the state in the query string. If they do not match, we will return a 418 unknown teapot error.

Next, we use the authorization code to generate a token. This token is used to create a HTTP client to make a request to GitHub API. So eventually we will send a GET request to https://api.github.com/user. This endpoint will provide us with the current authenticated user information in JSON format. We will decode this content and get the user's ID, login name (user name) and avatar URL.

Then we will try to find the user with the GitHub ID on the database. If it is not found, create a new one using this data.

Then, for the newly created user, we issue a JSON network token with user ID as the subject (Subject) and use that token to redirect to the front end, and the Expiration of the token is included in the query string.

This Web application will also be used in other posts, but the redirected link will be / callback?token=&expires_at=. There, we will use JavaScript to get tokens and expiration dates from URL, and make GET requests to / api/auth_user in the form of Bearer token_here through the tokens in the Authorization header to obtain authenticated users and save them to localStorage.

Guard middleware

In order to get the current authenticated users, we designed Guard middleware. This is because in the next article, we will have many endpoints that need to be authenticated, and middleware will allow us to share this functionality.

Type ContextKey struct {Name string} var keyAuthUserID = ContextKey {"auth_user_id"} func guard (handler http.HandlerFunc) http.HandlerFunc {return func (w http.ResponseWriter, r * http.Request) {var token string if a: = r.Header.Get ("Authorization"); strings.HasPrefix (a, "Bearer") {token = a [7:]} else if t: = r.URL.Query (). Get ("token") T! = "{token = t} else {http.Error (w, http.StatusText (http.StatusUnauthorized), http.StatusUnauthorized) return} var claims jwt.Claims if err: = jwtSigner.Decode ([] byte (token), & claims) Err! = nil {http.Error (w, http.StatusText (http.StatusUnauthorized), http.StatusUnauthorized) return} ctx: = r.Context () ctx = context.WithValue (ctx, keyAuthUserID, claims.Subject) handler (w, r.WithContext (ctx))}}

First, we try to read the token from the token field in the Authorization header or in the URL query string. If it is not found, we need to return a 401 Unauthorized error. We will then decode the declaration in the token and use the subject as the current authenticated user ID.

Now we can use this middleware to encapsulate any http.handlerFunc that needs to be authorized and keep the authenticated user ID in the context of the handler.

Var guarded = guard (func (w http.ResponseWriter, r * http.Request) {authUserID: = r.Context (). Value (keyAuthUserID). (string)}) obtain authenticated user func getAuthUser (w http.ResponseWriter, r * http.Request) {ctx: = r.Context () authUserID: = ctx.Value (keyAuthUserID). (string) var user User if err: = db.QueryRowContext (ctx, `SELECT username, avatar_url FROM users WHERE id = $1` AuthUserID) .Scan (& user.Username, & user.AvatarURL) Err = = sql.ErrNoRows {http.Error (w, http.StatusText (http.StatusTeapot), http.StatusTeapot) return} else if err! = nil {respondError (w, fmt.Errorf ("could not query auth user:% v", err)) return} user.ID = authUserID respond (w, user, http.StatusOK)}

We use Guard middleware to get the current authenticated user ID and query the database.

The above is about the content of this article on "how to build an instant messaging application in OAuth". I believe you all have some understanding. I hope the content shared by the editor will be helpful to you. If you want to know more about the relevant knowledge, please follow the industry information channel.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 218

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report