OAuth 2 Login With Google in Go Step by Step With Tests
Build Sign in with Google in Go with the authorization code flow, PKCE and state, tested end to end against a fake identity provider.

"Sign in with Google" is a request that arrives early in many projects, and the usual answer is a blog post that pastes a client ID and a callback handler and calls it done. The happy path is short. What deserves the effort is the rest: why each parameter exists, which checks are mandatory, and how to test a flow that crosses three parties without clicking through a real consent screen. This project builds the flow with golang.org/x/oauth2, tests it completely against a fake identity provider, and then shows exactly what changes to point it at Google.
Tested with Go 1.25.6, golang.org/x/oauth2 v0.34.0, a fake identity provider in the tests; Google's real endpoints were not called
It builds on cookies, sessions, authentication middleware and CSRF protection, and complements JWT handling since OpenID Connect tokens are JWTs. It belongs to the web cluster that ends in a production HTTP server. The code ran on Go 1.25.6 with x/oauth2 v0.34.0. Be clear about what was and was not exercised: every request in this article goes to a fake provider I wrote for the tests. I did not call Google's endpoints, so the Google-specific details come from its documentation and you should confirm them against the current version.
The cast and the flow
OAuth 2.0 delegates authorization, and OpenID Connect (OIDC) adds identity on top. Four parties appear. The user with a browser; your application, called the client; and the identity provider (Google), which authenticates the user and issues tokens. The authorization code flow with PKCE goes in seven steps:
- The user clicks "Sign in". Your app generates a random
stateand a PKCEcode_verifier, stores both in cookies, and redirects the browser to the provider's authorization endpoint with yourclient_id,redirect_uri,scope, thestateand thecode_challenge(a hash of the verifier). - The provider authenticates the user (login, consent screen) on its own domain.
- The provider redirects the browser back to your
redirect_uriwith a one-timecodeand the samestate. - Your callback checks that
statematches the cookie. - Your server posts the
codeand thecode_verifierto the provider's token endpoint and receives an access token (and, with OIDC, an ID token). - Your server calls the userinfo endpoint with the access token to learn who the user is.
- Your app creates its own session for that user and redirects to a page.
Two of these exist purely for security, and they are the ones to understand. state is a random value your app generates and checks on return. Without it, an attacker can complete a login with their own account and trick the victim's browser into loading your callback URL with the attacker's code, logging the victim into the attacker's account ("login CSRF"). PKCE (Proof Key for Code Exchange, RFC 7636) binds the code to the client that asked for it: only the party that knows the code_verifier can redeem the code, so a code intercepted from a redirect (a malicious app on the device, a leaked log) is useless. It started as a protection for mobile apps and is now recommended for every client, including confidential server-side ones.
A fake identity provider
Testing OAuth against the real thing means a browser, a Google account and a network. A fake provider that implements the three endpoints your app calls removes all of that, and writing it forces you to see precisely what the protocol requires. It validates the PKCE challenge, issues one-time codes, and serves user info:
package main
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"sync"
)
// fakeIdP is a minimal OpenID provider: /authorize, /token and /userinfo.
type fakeIdP struct {
*httptest.Server
mu sync.Mutex
codes map[string]authRequest // one-time authorization codes
tokens map[string]userInfo // access token -> user
EmailVerified bool
}
type authRequest struct{ redirectURI, challenge, state string }
type userInfo struct {
Sub string `json:"sub"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
Name string `json:"name"`
}
func randomString() string {
b := make([]byte, 16)
rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
func newIdP() *fakeIdP {
idp := &fakeIdP{codes: map[string]authRequest{}, tokens: map[string]userInfo{}, EmailVerified: true}
mux := http.NewServeMux()
// Step 2: the user would log in and consent here; the fake approves instantly.
mux.HandleFunc("GET /authorize", func(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if q.Get("response_type") != "code" || q.Get("code_challenge") == "" || q.Get("code_challenge_method") != "S256" {
http.Error(w, "invalid_request: PKCE with S256 is required", http.StatusBadRequest)
return
}
code := randomString()
idp.mu.Lock()
idp.codes[code] = authRequest{redirectURI: q.Get("redirect_uri"), challenge: q.Get("code_challenge"), state: q.Get("state")}
idp.mu.Unlock()
back, _ := url.Parse(q.Get("redirect_uri"))
v := back.Query()
v.Set("code", code)
v.Set("state", q.Get("state"))
back.RawQuery = v.Encode()
http.Redirect(w, r, back.String(), http.StatusFound) // step 3
})
// Step 5: the server-to-server exchange.
mux.HandleFunc("POST /token", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
idp.mu.Lock()
req, ok := idp.codes[r.PostForm.Get("code")]
delete(idp.codes, r.PostForm.Get("code")) // a code works once, whatever happens next
idp.mu.Unlock()
if !ok {
http.Error(w, `{"error":"invalid_grant"}`, http.StatusBadRequest)
return
}
sum := sha256.Sum256([]byte(r.PostForm.Get("code_verifier")))
if base64.RawURLEncoding.EncodeToString(sum[:]) != req.challenge || r.PostForm.Get("redirect_uri") != req.redirectURI {
http.Error(w, `{"error":"invalid_grant","error_description":"PKCE verification failed"}`, http.StatusBadRequest)
return
}
token := "at-" + randomString()
idp.mu.Lock()
idp.tokens[token] = userInfo{Sub: "google-oauth2|1234", Email: "[email protected]", EmailVerified: idp.EmailVerified, Name: "Ada Lovelace"}
idp.mu.Unlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"access_token": token, "token_type": "Bearer", "expires_in": 3600})
})
// Step 6.
mux.HandleFunc("GET /userinfo", func(w http.ResponseWriter, r *http.Request) {
idp.mu.Lock()
u, ok := idp.tokens[r.Header.Get("Authorization")[len("Bearer "):]]
idp.mu.Unlock()
if !ok {
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
json.NewEncoder(w).Encode(u)
})
idp.Server = httptest.NewServer(mux)
return idp
}Three behaviors of the fake are the protocol's own rules, and each is tested below. An authorization code can be used once (it is deleted on the first attempt, successful or not). The token endpoint recomputes the challenge from the supplied verifier and rejects a mismatch. And the redirect_uri at the token step must equal the one at the authorization step.
The application
The app has four handlers. Login starts the flow and remembers the two secrets in cookies; callback validates and completes it; /me shows the signed-in user; logout ends the session. Keeping the pieces in one App struct makes the provider replaceable:
package main
import (
"crypto/subtle"
"encoding/json"
"fmt"
"net/http"
"sync"
"golang.org/x/oauth2"
)
type App struct {
cfg *oauth2.Config
userinfoURL string
mu sync.Mutex
sessions map[string]userInfo // session ID -> user
}
func NewApp(cfg *oauth2.Config, userinfoURL string) *App {
return &App{cfg: cfg, userinfoURL: userinfoURL, sessions: map[string]userInfo{}}
}
func temp(name, value string, maxAge int) *http.Cookie {
return &http.Cookie{Name: name, Value: value, Path: "/", MaxAge: maxAge, HttpOnly: true, SameSite: http.SameSiteLaxMode}
}
func (a *App) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /login", a.login)
mux.HandleFunc("GET /callback", a.callback)
mux.HandleFunc("GET /me", a.me)
mux.HandleFunc("POST /logout", a.logout)
return mux
}
// Step 1: remember state and the PKCE verifier, then send the browser to the provider.
func (a *App) login(w http.ResponseWriter, r *http.Request) {
state := randomString()
verifier := oauth2.GenerateVerifier()
http.SetCookie(w, temp("oauth_state", state, 600))
http.SetCookie(w, temp("oauth_verifier", verifier, 600))
http.Redirect(w, r, a.cfg.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier)), http.StatusFound)
}
// Steps 4 to 7.
func (a *App) callback(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
if e := q.Get("error"); e != "" {
http.Error(w, "login was not completed: "+e, http.StatusBadRequest)
return
}
stateCookie, err1 := r.Cookie("oauth_state")
verifierCookie, err2 := r.Cookie("oauth_verifier")
if err1 != nil || err2 != nil {
http.Error(w, "the login session expired; start again", http.StatusBadRequest)
return
}
// Step 4: the state in the URL must be the one this browser started with.
if subtle.ConstantTimeCompare([]byte(q.Get("state")), []byte(stateCookie.Value)) != 1 {
http.Error(w, "state mismatch", http.StatusBadRequest)
return
}
// The temporary cookies have done their job.
http.SetCookie(w, temp("oauth_state", "", -1))
http.SetCookie(w, temp("oauth_verifier", "", -1))
// Step 5: exchange the code, proving possession of the verifier.
tok, err := a.cfg.Exchange(r.Context(), q.Get("code"), oauth2.VerifierOption(verifierCookie.Value))
if err != nil {
http.Error(w, "could not exchange the code", http.StatusBadGateway)
return
}
// Step 6: ask the provider who this is.
resp, err := a.cfg.Client(r.Context(), tok).Get(a.userinfoURL)
if err != nil || resp.StatusCode != http.StatusOK {
http.Error(w, "could not read the user profile", http.StatusBadGateway)
return
}
defer resp.Body.Close()
var u userInfo
if err := json.NewDecoder(resp.Body).Decode(&u); err != nil || u.Sub == "" {
http.Error(w, "unexpected profile response", http.StatusBadGateway)
return
}
if !u.EmailVerified {
http.Error(w, "your email address is not verified with the provider", http.StatusForbidden)
return
}
// Step 7: our own session. The provider's access token is not kept as the login.
id := randomString()
a.mu.Lock()
a.sessions[id] = u
a.mu.Unlock()
c := temp("app_session", id, 86400)
http.SetCookie(w, c)
http.Redirect(w, r, "/me", http.StatusSeeOther)
}
func (a *App) me(w http.ResponseWriter, r *http.Request) {
c, err := r.Cookie("app_session")
if err != nil {
http.Error(w, "not signed in", http.StatusUnauthorized)
return
}
a.mu.Lock()
u, ok := a.sessions[c.Value]
a.mu.Unlock()
if !ok {
http.Error(w, "not signed in", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "signed in as %s <%s> (provider id %s)", u.Name, u.Email, u.Sub)
}
func (a *App) logout(w http.ResponseWriter, r *http.Request) {
if c, err := r.Cookie("app_session"); err == nil {
a.mu.Lock()
delete(a.sessions, c.Value)
a.mu.Unlock()
}
http.SetCookie(w, temp("app_session", "", -1))
w.Write([]byte("signed out"))
}Read it with the seven steps beside it. oauth2.GenerateVerifier makes a high-entropy PKCE verifier; S256ChallengeOption derives and attaches the challenge to the authorization URL; VerifierOption supplies the verifier at exchange. Those three helpers are what turn PKCE from a hundred lines into three. The state comparison uses subtle.ConstantTimeCompare. The temporary cookies are HttpOnly and cleared as soon as the callback consumes them. The session is the app's own: a random ID in a cookie, backed by a store (session management covers expiry, rotation and Redis). Note what is deliberately absent: the provider's access token is used once to fetch the profile and is not saved or used as the login.
Running the whole flow
The demo starts the provider and the app, then plays the browser with a cookie jar that follows redirects. The one request it makes, GET /login, triggers the entire chain:
package main
import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"golang.org/x/oauth2"
)
func startApp(idp *fakeIdP) (*httptest.Server, *App) {
app := NewApp(&oauth2.Config{
ClientID: "my-client-id",
ClientSecret: "my-client-secret",
Scopes: []string{"openid", "email", "profile"},
Endpoint: oauth2.Endpoint{AuthURL: idp.URL + "/authorize", TokenURL: idp.URL + "/token"},
}, idp.URL+"/userinfo")
srv := httptest.NewServer(app.Routes())
app.cfg.RedirectURL = srv.URL + "/callback" // known only once the server is listening
return srv, app
}
func browse(client *http.Client, method, url string) (int, string) {
req, _ := http.NewRequest(method, url, nil)
resp, err := client.Do(req)
if err != nil {
return 0, err.Error()
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return resp.StatusCode, string(b)
}
func main() {
idp := newIdP()
defer idp.Close()
srv, _ := startApp(idp)
defer srv.Close()
jar, _ := cookiejar.New(nil)
browser := &http.Client{Jar: jar} // follows redirects, keeps cookies, like a browser
code, body := browse(browser, "GET", srv.URL+"/me")
fmt.Println("before login: ", code, body)
code, body = browse(browser, "GET", srv.URL+"/login") // redirects to the provider and back
fmt.Println("after /login: ", code, body)
code, body = browse(browser, "POST", srv.URL+"/logout")
fmt.Println("logout: ", code, body)
code, body = browse(browser, "GET", srv.URL+"/me")
fmt.Println("after logout: ", code, body)
}before login: 401 not signed in
after /login: 200 signed in as Ada Lovelace <[email protected]> (provider id google-oauth2|1234)
logout: 200 signed out
after logout: 401 not signed inThe single GET /login ran the whole dance: redirect to the fake provider, redirect back with a code, the callback verifying the state, exchanging the code with the PKCE verifier, reading the profile, creating the session, and a final 303 to /me, which the client followed and which answered with the signed-in user. Then logout destroys the session, and /me is a 401 again.
The attack cases, as tests
A flow that works on the happy path proves little about security. The tests below attack it: a tampered state, a missing cookie, a replayed code, a failed PKCE check and an unverified email. They are also where the fake provider pays off, since each attack is a few lines.
package main
import (
"net/http"
"net/http/cookiejar"
"net/http/httptest"
"net/url"
"testing"
)
// browserWithoutRedirects follows no redirects automatically, so a test can inspect each hop.
func manual(t *testing.T) (*http.Client, *cookiejar.Jar) {
jar, _ := cookiejar.New(nil)
return &http.Client{Jar: jar, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}, jar
}
func get(t *testing.T, c *http.Client, u string) *http.Response {
t.Helper()
resp, err := c.Get(u)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
return resp
}
// startLogin performs hops 1 and 2: /login, then the provider's /authorize. It returns the callback URL the provider sent.
func startLogin(t *testing.T, c *http.Client, app *httptest.Server) string {
t.Helper()
r1 := get(t, c, app.URL+"/login")
r2 := get(t, c, r1.Header.Get("Location"))
return r2.Header.Get("Location")
}
func TestSuccessfulLogin(t *testing.T) {
idp := newIdP()
defer idp.Close()
app, _ := startApp(idp)
defer app.Close()
c, _ := manual(t)
cb := get(t, c, startLogin(t, c, app))
if cb.StatusCode != http.StatusSeeOther {
t.Fatalf("callback answered %d, want 303", cb.StatusCode)
}
if me := get(t, c, app.URL+"/me"); me.StatusCode != http.StatusOK {
t.Fatalf("/me answered %d after login", me.StatusCode)
}
}
func TestTamperedStateIsRejected(t *testing.T) {
idp := newIdP()
defer idp.Close()
app, _ := startApp(idp)
defer app.Close()
c, _ := manual(t)
u, _ := url.Parse(startLogin(t, c, app))
q := u.Query()
q.Set("state", "attacker-chosen-state")
u.RawQuery = q.Encode()
if resp := get(t, c, u.String()); resp.StatusCode != http.StatusBadRequest {
t.Fatalf("a callback with the wrong state answered %d, want 400", resp.StatusCode)
}
}
func TestCallbackWithoutStartingALoginIsRejected(t *testing.T) {
idp := newIdP()
defer idp.Close()
app, _ := startApp(idp)
defer app.Close()
c, _ := manual(t)
if resp := get(t, c, app.URL+"/callback?code=x&state=y"); resp.StatusCode != http.StatusBadRequest {
t.Fatalf("a callback with no login cookies answered %d, want 400", resp.StatusCode)
}
}
func TestAuthorizationCodeCannotBeReused(t *testing.T) {
idp := newIdP()
defer idp.Close()
app, _ := startApp(idp)
defer app.Close()
c, jar := manual(t)
cb := startLogin(t, c, app)
u, _ := url.Parse(app.URL)
saved := jar.Cookies(u) // the state and verifier cookies, before the callback clears them
if first := get(t, c, cb); first.StatusCode != http.StatusSeeOther {
t.Fatalf("the first use of the code answered %d, want 303", first.StatusCode)
}
// A second browser replays the very same callback URL with the same cookies.
c2, jar2 := manual(t)
jar2.SetCookies(u, saved)
if replay := get(t, c2, cb); replay.StatusCode == http.StatusSeeOther {
t.Fatal("a used authorization code created a second session")
}
}
func TestWrongPKCEVerifierFailsTheExchange(t *testing.T) {
idp := newIdP()
defer idp.Close()
app, _ := startApp(idp)
defer app.Close()
c, jar := manual(t)
cb := startLogin(t, c, app)
u, _ := url.Parse(app.URL)
jar.SetCookies(u, []*http.Cookie{{Name: "oauth_verifier", Value: "a-verifier-the-provider-never-saw", Path: "/"}})
if resp := get(t, c, cb); resp.StatusCode != http.StatusBadGateway {
t.Fatalf("the exchange with the wrong verifier answered %d, want 502", resp.StatusCode)
}
}
func TestUnverifiedEmailIsRefused(t *testing.T) {
idp := newIdP()
defer idp.Close()
idp.EmailVerified = false
app, _ := startApp(idp)
defer app.Close()
c, _ := manual(t)
if resp := get(t, c, startLogin(t, c, app)); resp.StatusCode != http.StatusForbidden {
t.Fatalf("an unverified email answered %d, want 403", resp.StatusCode)
}
}=== RUN TestSuccessfulLogin
--- PASS: TestSuccessfulLogin (0.01s)
=== RUN TestTamperedStateIsRejected
--- PASS: TestTamperedStateIsRejected (0.00s)
=== RUN TestCallbackWithoutStartingALoginIsRejected
--- PASS: TestCallbackWithoutStartingALoginIsRejected (0.00s)
=== RUN TestAuthorizationCodeCannotBeReused
--- PASS: TestAuthorizationCodeCannotBeReused (0.00s)
=== RUN TestWrongPKCEVerifierFailsTheExchange
--- PASS: TestWrongPKCEVerifierFailsTheExchange (0.00s)
=== RUN TestUnverifiedEmailIsRefused
--- PASS: TestUnverifiedEmailIsRefused (0.00s)
PASS
ok example.test/lab 1.331sThe replay test is the most intricate: it logs in once so the code is consumed, then rebuilds the cookies a second login would carry and replays the same callback URL. The provider refuses the spent code, and the app must not create a session. Each of the other tests maps to one line of the earlier explanation: state is checked, the callback needs a login in progress, PKCE binds the exchange, the email must be verified.
Attack, defense, test
The security of the flow is the sum of a few specific checks, and it helps to keep the mapping from each attack to its defense and to the test that proves it:
| Attack | Defense in the code | Test |
|---|---|---|
| Login CSRF: the victim's browser completes the attacker's login | random state, compared with the cookie | TestTamperedStateIsRejected |
| Callback with no login in progress | require both temporary cookies | TestCallbackWithoutStartingALoginIsRejected |
| Stolen or replayed authorization code | one-time codes at the provider, PKCE verifier at the exchange | TestAuthorizationCodeCannotBeReused, TestWrongPKCEVerifierFailsTheExchange |
| Account takeover through an unverified email | require email_verified | TestUnverifiedEmailIsRefused |
| Open redirect after login | always redirect to a fixed path, never to a URL taken from the request | by construction |
| Token leakage | the provider's access token is not stored or used as the session | by construction |
A row without a test is a promise without evidence. If you add features, add rows: a "return to the page I came from" feature reintroduces open redirects unless the return path is validated as a local path, and a "link my account" feature reintroduces takeover unless it re-authenticates the existing account first.
Why this flow and not the others
Older tutorials show flows that are no longer recommended. The implicit flow returned tokens in the URL fragment, where they leak into browser history and scripts; the current guidance (RFC 9700) deprecates it. The resource owner password credentials flow had the user type their provider password into your app, which defeats the point of delegation. The authorization code flow with PKCE is the one the security guidance recommends for server-side web apps, single page apps and mobile apps alike, with a client secret added for confidential clients like this one. If you meet a library that offers "implicit" as the easy option, treat that as a reason to pick another library.
Errors the user will see
Login failures are common and mostly not your bug: the user clicks "Cancel" on the consent screen (the provider redirects back with error=access_denied), the login session cookie expired because they left the tab open for twenty minutes, the clock on a device is wrong, an admin blocked the app in a corporate workspace. Each deserves a message that tells the user what to do next, not a raw 400. The callback above returns plain text for brevity; in a real app, render a page with a "Try signing in again" link that restarts at /login, log the provider's error and error_description for yourself, and never reflect them into the page unescaped. Treat repeated state mismatches from one address as suspicious and count them.
Pointing it at Google
Everything specific to Google is configuration. In the Google Cloud console, create an OAuth client of type "Web application", add your callback URL (https://yourapp.example.com/callback) as an authorized redirect URI (it must match exactly, including scheme and path), and copy the client ID and secret into your configuration (never into the repository). In code:
import "golang.org/x/oauth2/google"
cfg := &oauth2.Config{
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
RedirectURL: "https://yourapp.example.com/callback",
Scopes: []string{"openid", "email", "profile"},
Endpoint: google.Endpoint,
}
app := NewApp(cfg, "https://openidconnect.googleapis.com/v1/userinfo")Those are the values from Google's documentation as I know them; confirm the endpoint URLs against the OpenID Connect discovery document and Google's OpenID Connect guide before relying on them, since I did not run against the live service. google.Endpoint comes from the golang.org/x/oauth2/google package, which this article did not exercise. The scopes request the user's email and basic profile and nothing more; ask for the least you need.
Sessions after login, and logging out
The provider has finished its job once you hold the profile. From then on the user is logged in to your application, with your session, your expiry and your logout. Two consequences surprise people. Logging out of your app does not log the user out of Google, and should not: they are signed in to Google for many other reasons, and you can only end your own session. And signing out of Google does not end your sessions either, unless you add back-channel logout, which is an extension for applications with stricter requirements. If you need to react to a revoked account or a changed password at the provider, refresh the profile periodically with a refresh token (request offline access deliberately and store the token encrypted) and end the local session if the provider reports a problem.
For many applications the simplest sufficient policy is a session of a day or a week with a sliding idle timeout, plus a "sign out everywhere" button that deletes all sessions for the user. Keep the session creation code path identical for password login and OAuth login, so the rest of the application, the middleware that reads the session and the authorization checks, does not care how the user arrived (authentication middleware).
What to add before production
Verify the ID token as well. OIDC returns an ID token (a signed JWT) with the token response. Verifying it (signature against the provider's published keys, iss, aud equal to your client ID, exp, and the nonce you sent) is the OIDC-specific proof of identity, and github.com/coreos/go-oidc does it with key discovery; the userinfo call here is simpler but relies on the access token alone (JWT pitfalls). Use sub as the account key. Emails change and can be reassigned; sub is stable per provider. Store (provider, sub) and treat the email as an attribute. Account linking needs a decision: a user who signed up with a password and later signs in with Google using the same email should be linked only after proving control of the existing account. Secure cookies: this demo runs on HTTP, so Secure is off; set it in production and use the __Host- prefix (cookies). Sessions need expiry, rotation at login and revocation (sessions). Rate limit and log the callback (rate limiting). And do not build your own provider: the fake exists to test your client; use a hosted or open-source identity provider if you need to issue identities.
The standards: RFC 6749 defines OAuth 2.0, RFC 7636 PKCE, and the IETF's OAuth 2.0 Security Best Current Practice lists the attacks this flow is designed around. The library documentation is at pkg.go.dev/golang.org/x/oauth2.


