Repository: markbates/goth
Pull request #570 (2024-07-26 12:13:12)

Add 1.22 mux support
This PR seems large; however, it fundamentally accomplishes 3 things.

1. Upgrade GO version to `1.22`
2. Add compatibility for the new 1.22 mux functionality to retrieve provider values
3. Removes all instances of `ioutil` with their non-deprecated `io` and `os` alternatives.

This this change and without any other modifications, this change breaks backward compatibility for GO <1.22. This can be fixed however by creating a new `gothic.go | gothic_test.go` with `//go:build go1.22` with these breaking changes. I am happy to make these compatibility changes but would love to get other's opinions first.

dgduncan: @techknowlogick I am open to ideas. I extracted out the `getProvider` functionality into two new files; each tagged with a version check. I would love to see if this now passes the tests of the pipeline. If you have a better idea for how to implement such backwards compatibility I would love to know. I am not super familiar with this type of backwards compatability.
dgduncan: Ok it looks like my change passes the tests; however, I still want to do some individual testing. Also I do not know how I feel about the `provider_legacy` naming. @techknowlogick do you have an opinion?

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8e64c21..f906ea2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,7 +13,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [ 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x ]
+        go-version: [ 1.18.x, 1.19.x, 1.20.x, 1.21.x, 1.22.x, 1.23.x, 1.24.x ]
         os: [ubuntu-latest, macos-latest, windows-latest]
     runs-on: ${{ matrix.os }}
     steps:
@@ -32,6 +32,6 @@ jobs:
           ${{ runner.os }}-go-
     - name: Format
       run: if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then exit 1; fi
-      if: matrix.os != 'windows-latest' && matrix.go-version == '1.22.x'
+      if: matrix.os != 'windows-latest' && matrix.go-version == '1.24.x'
     - name: Test
       run: go test -race ./...
diff --git a/.gitignore b/.gitignore
index 4f39cfc..5ce409a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ node_modules/
 dist/
 generated/
 .vendor/
+vendor
 *.swp
 .vscode/launch.json
 .vscode/settings.json
diff --git a/examples/main.go b/examples/main.go
index 6f421d8..8bb9ddf 100644
--- a/examples/main.go
+++ b/examples/main.go
@@ -113,9 +113,9 @@ func main() {
 		eveonline.New(os.Getenv("EVEONLINE_KEY"), os.Getenv("EVEONLINE_SECRET"), "http://localhost:3000/auth/eveonline/callback"),
 		kakao.New(os.Getenv("KAKAO_KEY"), os.Getenv("KAKAO_SECRET"), "http://localhost:3000/auth/kakao/callback"),
 
-		// Pointed localhost.com to http://localhost:3000/auth/yahoo/callback through proxy as yahoo
-		// does not allow to put custom ports in redirection uri
-		yahoo.New(os.Getenv("YAHOO_KEY"), os.Getenv("YAHOO_SECRET"), "http://localhost.com"),
+		// Pointed https://localhost.com to http://localhost:3000/auth/yahoo/callback
+		// Yahoo only accepts urls that starts with https
+		yahoo.New(os.Getenv("YAHOO_KEY"), os.Getenv("YAHOO_SECRET"), "https://localhost.com"),
 		typetalk.New(os.Getenv("TYPETALK_KEY"), os.Getenv("TYPETALK_SECRET"), "http://localhost:3000/auth/typetalk/callback", "my"),
 		slack.New(os.Getenv("SLACK_KEY"), os.Getenv("SLACK_SECRET"), "http://localhost:3000/auth/slack/callback"),
 		stripe.New(os.Getenv("STRIPE_KEY"), os.Getenv("STRIPE_SECRET"), "http://localhost:3000/auth/stripe/callback"),
diff --git a/go.mod b/go.mod
index bff421f..da843c0 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,8 @@ go 1.18
 
 require (
 	github.com/go-chi/chi/v5 v5.1.0
-	github.com/golang-jwt/jwt/v4 v4.2.0
+	github.com/golang-jwt/jwt/v4 v4.5.1
+	github.com/golang-jwt/jwt/v5 v5.2.1
 	github.com/gorilla/mux v1.6.2
 	github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1
 	github.com/gorilla/sessions v1.1.1
@@ -34,6 +35,7 @@ require (
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rogpeppe/go-internal v1.12.0 // indirect
+	github.com/stretchr/objx v0.5.2 // indirect
 	golang.org/x/crypto v0.21.0 // indirect
 	google.golang.org/appengine v1.6.8 // indirect
 	google.golang.org/protobuf v1.32.0 // indirect
diff --git a/go.sum b/go.sum
index 75497ee..7508d60 100644
--- a/go.sum
+++ b/go.sum
@@ -13,14 +13,17 @@ github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
 github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
 github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
 github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
-github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
-github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
+github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
+github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
+github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
 github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
 github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
@@ -68,6 +71,7 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
diff --git a/gothic/gothic.go b/gothic/gothic.go
index 0c32caf..b895a12 100644
--- a/gothic/gothic.go
+++ b/gothic/gothic.go
@@ -16,14 +16,11 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
 	"strings"
 
-	"github.com/go-chi/chi/v5"
-	"github.com/gorilla/mux"
 	"github.com/gorilla/sessions"
 	"github.com/markbates/goth"
 )
@@ -254,60 +251,6 @@ func Logout(res http.ResponseWriter, req *http.Request) error {
 	return nil
 }
 
-// GetProviderName is a function used to get the name of a provider
-// for a given request. By default, this provider is fetched from
-// the URL query string. If you provide it in a different way,
-// assign your own function to this variable that returns the provider
-// name for your request.
-var GetProviderName = getProviderName
-
-func getProviderName(req *http.Request) (string, error) {
-
-	// try to get it from the url param "provider"
-	if p := req.URL.Query().Get("provider"); p != "" {
-		return p, nil
-	}
-
-	// try to get it from the url param ":provider"
-	if p := req.URL.Query().Get(":provider"); p != "" {
-		return p, nil
-	}
-
-	// try to get it from the context's value of "provider" key
-	if p, ok := mux.Vars(req)["provider"]; ok {
-		return p, nil
-	}
-
-	//  try to get it from the go-context's value of "provider" key
-	if p, ok := req.Context().Value("provider").(string); ok {
-		return p, nil
-	}
-
-	// try to get it from the url param "provider", when req is routed through 'chi'
-	if p := chi.URLParam(req, "provider"); p != "" {
-		return p, nil
-	}
-
-	// try to get it from the go-context's value of providerContextKey key
-	if p, ok := req.Context().Value(ProviderParamKey).(string); ok {
-		return p, nil
-	}
-
-	// As a fallback, loop over the used providers, if we already have a valid session for any provider (ie. user has already begun authentication with a provider), then return that provider name
-	providers := goth.GetProviders()
-	session, _ := Store.Get(req, SessionName)
-	for _, provider := range providers {
-		p := provider.Name()
-		value := session.Values[p]
-		if _, ok := value.(string); ok {
-			return p, nil
-		}
-	}
-
-	// if not found then return an empty string with the corresponding error
-	return "", errors.New("you must select a provider")
-}
-
 // GetContextWithProvider returns a new request context containing the provider
 func GetContextWithProvider(req *http.Request, provider string) *http.Request {
 	return req.WithContext(context.WithValue(req.Context(), ProviderParamKey, provider))
@@ -347,7 +290,7 @@ func getSessionValue(session *sessions.Session, key string) (string, error) {
 	if err != nil {
 		return "", err
 	}
-	s, err := ioutil.ReadAll(r)
+	s, err := io.ReadAll(r)
 	if err != nil {
 		return "", err
 	}
diff --git a/gothic/provider.go b/gothic/provider.go
new file mode 100644
index 0000000..60bc1eb
--- /dev/null
+++ b/gothic/provider.go
@@ -0,0 +1,71 @@
+//go:build go1.22
+// +build go1.22
+
+package gothic
+
+import (
+	"errors"
+	"net/http"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/gorilla/mux"
+	"github.com/markbates/goth"
+)
+
+// GetProviderName is a function used to get the name of a provider
+// for a given request. By default, this provider is fetched from
+// the URL query string. If you provide it in a different way,
+// assign your own function to this variable that returns the provider
+// name for your request.
+var GetProviderName = getProviderName
+
+func getProviderName(req *http.Request) (string, error) {
+	// try to get it from the url param "provider"
+	if p := req.URL.Query().Get("provider"); p != "" {
+		return p, nil
+	}
+
+	// try to get it from the url param ":provider"
+	if p := req.URL.Query().Get(":provider"); p != "" {
+		return p, nil
+	}
+
+	// try to get it from the context's value of "provider" key
+	if p, ok := mux.Vars(req)["provider"]; ok {
+		return p, nil
+	}
+
+	//  try to get it from the go-context's value of "provider" key
+	if p, ok := req.Context().Value("provider").(string); ok {
+		return p, nil
+	}
+
+	// try to get it from the url param "provider", when req is routed through 'chi'
+	if p := chi.URLParam(req, "provider"); p != "" {
+		return p, nil
+	}
+
+	// try to get it from the route param for go >= 1.22
+	if p := req.PathValue("provider"); p != "" {
+		return p, nil
+	}
+
+	// try to get it from the go-context's value of providerContextKey key
+	if p, ok := req.Context().Value(ProviderParamKey).(string); ok {
+		return p, nil
+	}
+
+	// As a fallback, loop over the used providers, if we already have a valid session for any provider (ie. user has already begun authentication with a provider), then return that provider name
+	providers := goth.GetProviders()
+	session, _ := Store.Get(req, SessionName)
+	for _, provider := range providers {
+		p := provider.Name()
+		value := session.Values[p]
+		if _, ok := value.(string); ok {
+			return p, nil
+		}
+	}
+
+	// if not found then return an empty string with the corresponding error
+	return "", errors.New("you must select a provider")
+}
diff --git a/gothic/provider_legacy.go b/gothic/provider_legacy.go
new file mode 100644
index 0000000..74cfcff
--- /dev/null
+++ b/gothic/provider_legacy.go
@@ -0,0 +1,66 @@
+//go:build !go1.22
+// +build !go1.22
+
+package gothic
+
+import (
+	"errors"
+	"net/http"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/gorilla/mux"
+	"github.com/markbates/goth"
+)
+
+// GetProviderName is a function used to get the name of a provider
+// for a given request. By default, this provider is fetched from
+// the URL query string. If you provide it in a different way,
+// assign your own function to this variable that returns the provider
+// name for your request.
+var GetProviderName = getProviderName
+
+func getProviderName(req *http.Request) (string, error) {
+	// try to get it from the url param "provider"
+	if p := req.URL.Query().Get("provider"); p != "" {
+		return p, nil
+	}
+
+	// try to get it from the url param ":provider"
+	if p := req.URL.Query().Get(":provider"); p != "" {
+		return p, nil
+	}
+
+	// try to get it from the context's value of "provider" key
+	if p, ok := mux.Vars(req)["provider"]; ok {
+		return p, nil
+	}
+
+	//  try to get it from the go-context's value of "provider" key
+	if p, ok := req.Context().Value("provider").(string); ok {
+		return p, nil
+	}
+
+	// try to get it from the url param "provider", when req is routed through 'chi'
+	if p := chi.URLParam(req, "provider"); p != "" {
+		return p, nil
+	}
+
+	// try to get it from the go-context's value of providerContextKey key
+	if p, ok := req.Context().Value(ProviderParamKey).(string); ok {
+		return p, nil
+	}
+
+	// As a fallback, loop over the used providers, if we already have a valid session for any provider (ie. user has already begun authentication with a provider), then return that provider name
+	providers := goth.GetProviders()
+	session, _ := Store.Get(req, SessionName)
+	for _, provider := range providers {
+		p := provider.Name()
+		value := session.Values[p]
+		if _, ok := value.(string); ok {
+			return p, nil
+		}
+	}
+
+	// if not found then return an empty string with the corresponding error
+	return "", errors.New("you must select a provider")
+}
diff --git a/provider.go b/provider.go
index 4635a42..1aaf1b4 100644
--- a/provider.go
+++ b/provider.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"sync"
 
 	"golang.org/x/oauth2"
 )
@@ -23,15 +24,21 @@ type Provider interface {
 
 const NoAuthUrlErrorMessage = "an AuthURL has not been set"
 
-// Providers is list of known/available providers.
+// Providers is the list of known/available providers.
 type Providers map[string]Provider
 
-var providers = Providers{}
+var (
+	providersHat sync.RWMutex
+	providers    = Providers{}
+)
 
 // UseProviders adds a list of available providers for use with Goth.
 // Can be called multiple times. If you pass the same provider more
 // than once, the last will be used.
 func UseProviders(viders ...Provider) {
+	providersHat.Lock()
+	defer providersHat.Unlock()
+
 	for _, provider := range viders {
 		providers[provider.Name()] = provider
 	}
@@ -45,7 +52,9 @@ func GetProviders() Providers {
 // GetProvider returns a previously created provider. If Goth has not
 // been told to use the named provider it will return an error.
 func GetProvider(name string) (Provider, error) {
+	providersHat.RLock()
 	provider := providers[name]
+	providersHat.RUnlock()
 	if provider == nil {
 		return nil, fmt.Errorf("no provider for %s exists", name)
 	}
@@ -55,6 +64,9 @@ func GetProvider(name string) (Provider, error) {
 // ClearProviders will remove all providers currently in use.
 // This is useful, mostly, for testing purposes.
 func ClearProviders() {
+	providersHat.Lock()
+	defer providersHat.Unlock()
+
 	providers = Providers{}
 }
 
diff --git a/providers/amazon/amazon.go b/providers/amazon/amazon.go
index 7061e4d..5a0b175 100644
--- a/providers/amazon/amazon.go
+++ b/providers/amazon/amazon.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 
@@ -95,7 +94,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/apple/apple.go b/providers/apple/apple.go
index 9ab85b4..cd3926b 100644
--- a/providers/apple/apple.go
+++ b/providers/apple/apple.go
@@ -13,7 +13,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/golang-jwt/jwt/v4"
+	"github.com/golang-jwt/jwt/v5"
 	"github.com/markbates/goth"
 	"golang.org/x/oauth2"
 )
diff --git a/providers/apple/session.go b/providers/apple/session.go
index e80e1ba..becfef3 100644
--- a/providers/apple/session.go
+++ b/providers/apple/session.go
@@ -10,7 +10,7 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/golang-jwt/jwt/v4"
+	"github.com/golang-jwt/jwt/v5"
 	"github.com/lestrrat-go/jwx/jwk"
 	"github.com/markbates/goth"
 	"golang.org/x/oauth2"
@@ -48,7 +48,7 @@ func (s Session) Marshal() string {
 }
 
 type IDTokenClaims struct {
-	jwt.StandardClaims
+	jwt.RegisteredClaims
 	AccessTokenHash string     `json:"at_hash"`
 	AuthTime        int        `json:"auth_time"`
 	Email           string     `json:"email"`
@@ -80,17 +80,10 @@ func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string,
 		idToken, err := jwt.ParseWithClaims(idToken.(string), &IDTokenClaims{}, func(t *jwt.Token) (interface{}, error) {
 			kid := t.Header["kid"].(string)
 			claims := t.Claims.(*IDTokenClaims)
-			vErr := new(jwt.ValidationError)
-			if !claims.VerifyAudience(p.clientId, true) {
-				vErr.Inner = fmt.Errorf("audience is incorrect")
-				vErr.Errors |= jwt.ValidationErrorAudience
-			}
-			if !claims.VerifyIssuer(AppleAudOrIss, true) {
-				vErr.Inner = fmt.Errorf("issuer is incorrect")
-				vErr.Errors |= jwt.ValidationErrorIssuer
-			}
-			if vErr.Errors > 0 {
-				return nil, vErr
+			validator := jwt.NewValidator(jwt.WithAudience(p.clientId), jwt.WithIssuer(AppleAudOrIss))
+			err := validator.Validate(claims)
+			if err != nil {
+				return nil, err
 			}
 
 			// per OpenID Connect Core 1.0 §3.2.2.9, Access Token Validation
@@ -98,9 +91,7 @@ func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string,
 			halfHash := hash[0:(len(hash) / 2)]
 			encodedHalfHash := base64.RawURLEncoding.EncodeToString(halfHash)
 			if encodedHalfHash != claims.AccessTokenHash {
-				vErr.Inner = fmt.Errorf(`identity token invalid`)
-				vErr.Errors |= jwt.ValidationErrorClaimsInvalid
-				return nil, vErr
+				return nil, fmt.Errorf(`identity token invalid`)
 			}
 
 			// get the public key for verifying the identity token signature
diff --git a/providers/azureadv2/azureadv2.go b/providers/azureadv2/azureadv2.go
index 523f79c..e76419f 100644
--- a/providers/azureadv2/azureadv2.go
+++ b/providers/azureadv2/azureadv2.go
@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -199,7 +198,7 @@ func userFromReader(r io.Reader, user *goth.User) error {
 		UserPrincipalName string   `json:"userPrincipalName"` // The user's principal name.
 	}{}
 
-	userBytes, err := ioutil.ReadAll(r)
+	userBytes, err := io.ReadAll(r)
 	if err != nil {
 		return err
 	}
diff --git a/providers/battlenet/battlenet.go b/providers/battlenet/battlenet.go
index 3a52b47..47abdac 100644
--- a/providers/battlenet/battlenet.go
+++ b/providers/battlenet/battlenet.go
@@ -6,7 +6,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -103,7 +103,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/bitbucket/bitbucket.go b/providers/bitbucket/bitbucket.go
index 06f2c96..7c27a91 100644
--- a/providers/bitbucket/bitbucket.go
+++ b/providers/bitbucket/bitbucket.go
@@ -5,7 +5,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -134,7 +134,7 @@ func (p *Provider) getUserInfo(user *goth.User, sess *Session) error {
 		return fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return err
 	}
diff --git a/providers/bitly/bitly.go b/providers/bitly/bitly.go
index f302d5f..fc1b122 100644
--- a/providers/bitly/bitly.go
+++ b/providers/bitly/bitly.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -97,7 +96,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 	}
 	defer resp.Body.Close()
 
-	buf, err := ioutil.ReadAll(resp.Body)
+	buf, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return u, err
 	}
diff --git a/providers/classlink/provider.go b/providers/classlink/provider.go
index a64a67c..1dc6836 100644
--- a/providers/classlink/provider.go
+++ b/providers/classlink/provider.go
@@ -4,7 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -102,7 +102,7 @@ func (p Provider) FetchUser(session goth.Session) (goth.User, error) {
 
 	defer resp.Body.Close()
 
-	bytes, err := ioutil.ReadAll(resp.Body)
+	bytes, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/cloudfoundry/cf.go b/providers/cloudfoundry/cf.go
index 31eb63f..5c06763 100644
--- a/providers/cloudfoundry/cf.go
+++ b/providers/cloudfoundry/cf.go
@@ -8,7 +8,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"strings"
 
@@ -104,7 +103,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(resp.Body)
+	bits, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/cognito/cognito.go b/providers/cognito/cognito.go
index ff4bee5..ea4b235 100644
--- a/providers/cognito/cognito.go
+++ b/providers/cognito/cognito.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -128,7 +127,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/dailymotion/dailymotion.go b/providers/dailymotion/dailymotion.go
index d3c1e1d..a8657a5 100644
--- a/providers/dailymotion/dailymotion.go
+++ b/providers/dailymotion/dailymotion.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 
@@ -97,7 +96,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/deezer/deezer.go b/providers/deezer/deezer.go
index a0f9b9d..7f5ed1f 100644
--- a/providers/deezer/deezer.go
+++ b/providers/deezer/deezer.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -98,7 +97,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/digitalocean/digitalocean.go b/providers/digitalocean/digitalocean.go
index 69a7a63..560952f 100644
--- a/providers/digitalocean/digitalocean.go
+++ b/providers/digitalocean/digitalocean.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -105,7 +104,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(resp.Body)
+	bits, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/discord/discord.go b/providers/discord/discord.go
index 9b3030f..75afd09 100644
--- a/providers/discord/discord.go
+++ b/providers/discord/discord.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -143,7 +142,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(resp.Body)
+	bits, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/dropbox/dropbox.go b/providers/dropbox/dropbox.go
index da55dba..e02dce8 100644
--- a/providers/dropbox/dropbox.go
+++ b/providers/dropbox/dropbox.go
@@ -7,7 +7,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"strings"
 
@@ -105,7 +104,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(resp.Body)
+	bits, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/eveonline/eveonline.go b/providers/eveonline/eveonline.go
index ad18776..6bf1563 100644
--- a/providers/eveonline/eveonline.go
+++ b/providers/eveonline/eveonline.go
@@ -6,7 +6,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -102,7 +102,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/facebook/facebook.go b/providers/facebook/facebook.go
index 23773ca..f740f46 100644
--- a/providers/facebook/facebook.go
+++ b/providers/facebook/facebook.go
@@ -11,7 +11,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
@@ -127,7 +126,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/gitea/gitea.go b/providers/gitea/gitea.go
index 6156df1..d04f204 100644
--- a/providers/gitea/gitea.go
+++ b/providers/gitea/gitea.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -115,7 +114,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/github/github.go b/providers/github/github.go
index 962d07a..37efff9 100644
--- a/providers/github/github.go
+++ b/providers/github/github.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"strconv"
 	"strings"
@@ -126,7 +125,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("GitHub API responded with a %d trying to fetch user information", response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/gitlab/gitlab.go b/providers/gitlab/gitlab.go
index e5457cf..e3561eb 100644
--- a/providers/gitlab/gitlab.go
+++ b/providers/gitlab/gitlab.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -116,7 +115,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/google/google.go b/providers/google/google.go
index a266c91..629afb4 100644
--- a/providers/google/google.go
+++ b/providers/google/google.go
@@ -5,7 +5,7 @@ package google
 import (
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/url"
 	"strings"
@@ -110,7 +110,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	responseBytes, err := ioutil.ReadAll(response.Body)
+	responseBytes, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/gplus/gplus.go b/providers/gplus/gplus.go
index 61e9a0e..832d723 100644
--- a/providers/gplus/gplus.go
+++ b/providers/gplus/gplus.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
@@ -102,7 +101,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/hubspot/hubspot.go b/providers/hubspot/hubspot.go
index 10b292d..36b5dd8 100644
--- a/providers/hubspot/hubspot.go
+++ b/providers/hubspot/hubspot.go
@@ -3,13 +3,14 @@ package hubspot
 import (
 	"encoding/json"
 	"fmt"
-	"github.com/markbates/goth"
-	"golang.org/x/oauth2"
 	"io"
 	"net/http"
 	"net/url"
 	"strconv"
 	"time"
+
+	"github.com/markbates/goth"
+	"golang.org/x/oauth2"
 )
 
 // These vars define the Authentication and Token URLS for Hubspot.
diff --git a/providers/influxcloud/influxcloud.go b/providers/influxcloud/influxcloud.go
index 7f7ad94..220cb10 100644
--- a/providers/influxcloud/influxcloud.go
+++ b/providers/influxcloud/influxcloud.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -129,7 +128,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/instagram/instagram.go b/providers/instagram/instagram.go
index b4703dc..0d1c9cc 100644
--- a/providers/instagram/instagram.go
+++ b/providers/instagram/instagram.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 
@@ -97,7 +96,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/intercom/intercom.go b/providers/intercom/intercom.go
index b58e864..4d2e278 100644
--- a/providers/intercom/intercom.go
+++ b/providers/intercom/intercom.go
@@ -7,7 +7,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"strings"
 
@@ -105,7 +104,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/kakao/kakao.go b/providers/kakao/kakao.go
index f19e35a..15d97c4 100644
--- a/providers/kakao/kakao.go
+++ b/providers/kakao/kakao.go
@@ -6,7 +6,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"strconv"
 
@@ -105,7 +105,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/lark/lark.go b/providers/lark/lark.go
new file mode 100644
index 0000000..d9900b9
--- /dev/null
+++ b/providers/lark/lark.go
@@ -0,0 +1,307 @@
+package lark
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/markbates/goth"
+	"golang.org/x/oauth2"
+)
+
+const (
+	appAccessTokenURL string = "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal/" // get app_access_token
+
+	authURL         string = "https://open.feishu.cn/open-apis/authen/v1/authorize"                 // obtain authorization code
+	tokenURL        string = "https://open.feishu.cn/open-apis/authen/v1/oidc/access_token"         // get user_access_token
+	refreshTokenURL string = "https://open.feishu.cn/open-apis/authen/v1/oidc/refresh_access_token" // refresh user_access_token
+	endpointProfile string = "https://open.feishu.cn/open-apis/authen/v1/user_info"                 // get user info
+)
+
+// Lark is the implementation of `goth.Provider` for accessing Lark
+type Lark interface {
+	GetAppAccessToken() error // get app access token
+}
+
+// Provider is the implementation of `goth.Provider` for accessing Lark
+type Provider struct {
+	ClientKey    string
+	Secret       string
+	CallbackURL  string
+	HTTPClient   *http.Client
+	config       *oauth2.Config
+	providerName string
+
+	appAccessToken *appAccessToken
+}
+
+// New creates a new Lark provider and sets up important connection details.
+func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
+	p := &Provider{
+		ClientKey:      clientKey,
+		Secret:         secret,
+		CallbackURL:    callbackURL,
+		providerName:   "lark",
+		appAccessToken: &appAccessToken{},
+	}
+	p.config = newConfig(p, authURL, tokenURL, scopes)
+	return p
+}
+
+func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config {
+	c := &oauth2.Config{
+		ClientID:     provider.ClientKey,
+		ClientSecret: provider.Secret,
+		RedirectURL:  provider.CallbackURL,
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  authURL,
+			TokenURL: tokenURL,
+		},
+		Scopes: []string{},
+	}
+
+	if len(scopes) > 0 {
+		c.Scopes = append(c.Scopes, scopes...)
+	}
+	return c
+}
+
+func (p *Provider) Client() *http.Client {
+	return goth.HTTPClientWithFallBack(p.HTTPClient)
+}
+
+func (p *Provider) Name() string {
+	return p.providerName
+}
+
+func (p *Provider) SetName(name string) {
+	p.providerName = name
+}
+
+type appAccessToken struct {
+	Token     string
+	ExpiresAt time.Time
+	rMutex    sync.RWMutex
+}
+
+type appAccessTokenReq struct {
+	AppID     string `json:"app_id"`     // 自建应用的 app_id
+	AppSecret string `json:"app_secret"` // 自建应用的 app_secret
+}
+
+type appAccessTokenResp struct {
+	Code           int    `json:"code"`             // 错误码
+	Msg            string `json:"msg"`              // 错误信息
+	AppAccessToken string `json:"app_access_token"` // 用于调用应用级接口的 app_access_token
+	Expire         int64  `json:"expire"`           // app_access_token 的过期时间
+}
+
+// GetAppAccessToken get lark app access token
+func (p *Provider) GetAppAccessToken() error {
+	// get from cache app access token
+	p.appAccessToken.rMutex.RLock()
+	if time.Now().Before(p.appAccessToken.ExpiresAt) {
+		p.appAccessToken.rMutex.RUnlock()
+		return nil
+	}
+	p.appAccessToken.rMutex.RUnlock()
+
+	reqBody, err := json.Marshal(&appAccessTokenReq{
+		AppID:     p.ClientKey,
+		AppSecret: p.Secret,
+	})
+	if err != nil {
+		return fmt.Errorf("failed to marshal request body: %w", err)
+	}
+
+	req, err := http.NewRequest(http.MethodPost, appAccessTokenURL, bytes.NewBuffer(reqBody))
+	if err != nil {
+		return fmt.Errorf("failed to create app access token request: %w", err)
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := p.Client().Do(req)
+	if err != nil {
+		return fmt.Errorf("failed to send app access token request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("unexpected status code while fetching app access token: %d", resp.StatusCode)
+	}
+
+	tokenResp := new(appAccessTokenResp)
+	if err = json.NewDecoder(resp.Body).Decode(tokenResp); err != nil {
+		return fmt.Errorf("failed to decode app access token response: %w", err)
+	}
+
+	if tokenResp.Code != 0 {
+		return fmt.Errorf("failed to get app access token: code:%v msg: %s", tokenResp.Code, tokenResp.Msg)
+	}
+
+	// update local cache
+	expirationDuration := time.Duration(tokenResp.Expire) * time.Second
+	p.appAccessToken.rMutex.Lock()
+	p.appAccessToken.Token = tokenResp.AppAccessToken
+	p.appAccessToken.ExpiresAt = time.Now().Add(expirationDuration)
+	p.appAccessToken.rMutex.Unlock()
+
+	return nil
+}
+
+func (p *Provider) BeginAuth(state string) (goth.Session, error) {
+	// build lark auth url
+	u, err := url.Parse(p.config.AuthCodeURL(state))
+	if err != nil {
+		panic(err)
+	}
+	query := u.Query()
+	query.Del("response_type")
+	query.Del("client_id")
+	query.Add("app_id", p.ClientKey)
+	u.RawQuery = query.Encode()
+
+	return &Session{
+		AuthURL: u.String(),
+	}, nil
+}
+
+func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
+	s := &Session{}
+	err := json.NewDecoder(strings.NewReader(data)).Decode(s)
+	return s, err
+}
+
+func (p *Provider) Debug(b bool) {
+}
+
+type getUserAccessTokenResp struct {
+	AccessToken      string `json:"access_token"`
+	RefreshToken     string `json:"refresh_token"`
+	TokenType        string `json:"token_type"`
+	ExpiresIn        int    `json:"expires_in"`
+	RefreshExpiresIn int    `json:"refresh_expires_in"`
+	Scope            string `json:"scope"`
+}
+
+func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
+	if err := p.GetAppAccessToken(); err != nil {
+		return nil, fmt.Errorf("failed to get app access token: %w", err)
+	}
+	reqBody := strings.NewReader(`{"grant_type":"refresh_token","refresh_token":"` + refreshToken + `"}`)
+
+	req, err := http.NewRequest(http.MethodPost, refreshTokenURL, reqBody)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create refresh token request: %w", err)
+	}
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", p.appAccessToken.Token))
+
+	resp, err := p.Client().Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("failed to send refresh token request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return nil, fmt.Errorf("unexpected status code while refreshing token: %d", resp.StatusCode)
+	}
+
+	var oauthResp commResponse[getUserAccessTokenResp]
+	err = json.NewDecoder(resp.Body).Decode(&oauthResp)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decode refreshed token: %w", err)
+	}
+	if oauthResp.Code != 0 {
+		return nil, fmt.Errorf("failed to refresh token: code:%v msg: %s", oauthResp.Code, oauthResp.Msg)
+	}
+
+	token := oauth2.Token{
+		AccessToken:  oauthResp.Data.AccessToken,
+		RefreshToken: oauthResp.Data.RefreshToken,
+		Expiry:       time.Now().Add(time.Duration(oauthResp.Data.ExpiresIn) * time.Second),
+	}
+
+	return &token, nil
+}
+
+func (p *Provider) RefreshTokenAvailable() bool {
+	return true
+}
+
+type commResponse[T any] struct {
+	Code int    `json:"code"`
+	Msg  string `json:"msg"`
+	Data T      `json:"data"`
+}
+
+type larkUser struct {
+	OpenID    string `json:"open_id"`
+	UnionID   string `json:"union_id"`
+	UserID    string `json:"user_id"`
+	Name      string `json:"name"`
+	Email     string `json:"enterprise_email"`
+	AvatarURL string `json:"avatar_url"`
+	Mobile    string `json:"mobile,omitempty"`
+}
+
+// FetchUser will go to Lark and access basic information about the user.
+func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
+	sess := session.(*Session)
+	user := goth.User{
+		AccessToken:  sess.AccessToken,
+		Provider:     p.Name(),
+		RefreshToken: sess.RefreshToken,
+		ExpiresAt:    sess.ExpiresAt,
+	}
+	if user.AccessToken == "" {
+		return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
+	}
+
+	req, err := http.NewRequest("GET", endpointProfile, nil)
+	if err != nil {
+		return user, fmt.Errorf("%s failed to create request: %w", p.providerName, err)
+	}
+	req.Header.Set("Authorization", "Bearer "+user.AccessToken)
+
+	resp, err := p.Client().Do(req)
+	if err != nil {
+		return user, fmt.Errorf("%s failed to get user information: %w", p.providerName, err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
+	}
+
+	responseBytes, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return user, fmt.Errorf("failed to read response body: %w", err)
+	}
+
+	var oauthResp commResponse[larkUser]
+	if err = json.Unmarshal(responseBytes, &oauthResp); err != nil {
+		return user, fmt.Errorf("failed to decode user info: %w", err)
+	}
+	if oauthResp.Code != 0 {
+		return user, fmt.Errorf("failed to get user info: code:%v msg: %s", oauthResp.Code, oauthResp.Msg)
+	}
+
+	u := oauthResp.Data
+	user.UserID = u.UserID
+	user.Name = u.Name
+	user.Email = u.Email
+	user.AvatarURL = u.AvatarURL
+	user.NickName = u.Name
+
+	if err = json.Unmarshal(responseBytes, &user.RawData); err != nil {
+		return user, err
+	}
+	return user, nil
+}
diff --git a/providers/lark/session.go b/providers/lark/session.go
new file mode 100644
index 0000000..2fdf260
--- /dev/null
+++ b/providers/lark/session.go
@@ -0,0 +1,71 @@
+package lark
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/markbates/goth"
+)
+
+type Session struct {
+	AuthURL               string
+	AccessToken           string
+	RefreshToken          string
+	ExpiresAt             time.Time
+	RefreshTokenExpiresAt time.Time
+}
+
+func (s *Session) GetAuthURL() (string, error) {
+	if s.AuthURL == "" {
+		return "", errors.New("lark: missing AuthURL")
+	}
+	return s.AuthURL, nil
+}
+
+func (s *Session) Marshal() string {
+	b, _ := json.Marshal(s)
+	return string(b)
+}
+
+func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
+	p := provider.(*Provider)
+	reqBody := strings.NewReader(`{"grant_type":"authorization_code","code":"` + params.Get("code") + `"}`)
+	req, err := http.NewRequest(http.MethodPost, tokenURL, reqBody)
+	if err != nil {
+		return "", fmt.Errorf("failed to create refresh token request: %w", err)
+	}
+	if err = p.GetAppAccessToken(); err != nil {
+		return "", fmt.Errorf("failed to get app access token: %w", err)
+	}
+	req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", p.appAccessToken.Token))
+	req.Header.Add("Content-Type", "application/json; charset=utf-8")
+
+	resp, err := p.Client().Do(req)
+	if err != nil {
+		return "", fmt.Errorf("failed to send refresh token request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return "", fmt.Errorf("unexpected status code while authorizing: %d", resp.StatusCode)
+	}
+
+	var larkCommResp commResponse[getUserAccessTokenResp]
+	err = json.NewDecoder(resp.Body).Decode(&larkCommResp)
+	if err != nil {
+		return "", fmt.Errorf("failed to decode commResponse: %w", err)
+	}
+	if larkCommResp.Code != 0 {
+		return "", fmt.Errorf("failed to get accessToken: code:%v msg: %s", larkCommResp.Code, larkCommResp.Msg)
+	}
+
+	s.AccessToken = larkCommResp.Data.AccessToken
+	s.RefreshToken = larkCommResp.Data.RefreshToken
+	s.ExpiresAt = time.Now().Add(time.Duration(larkCommResp.Data.ExpiresIn) * time.Second)
+	s.RefreshTokenExpiresAt = time.Now().Add(time.Duration(larkCommResp.Data.RefreshExpiresIn) * time.Second)
+	return s.AccessToken, nil
+}
diff --git a/providers/lastfm/lastfm.go b/providers/lastfm/lastfm.go
index ad30e8d..3d84445 100644
--- a/providers/lastfm/lastfm.go
+++ b/providers/lastfm/lastfm.go
@@ -8,7 +8,7 @@ import (
 	"encoding/xml"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/url"
 	"sort"
@@ -168,7 +168,7 @@ func (p *Provider) request(sign bool, params map[string]string, result interface
 		err = errors.New(fmt.Errorf("Request error(%v) %v", res.StatusCode, res.Status).Error())
 		return err
 	}
-	body, err := ioutil.ReadAll(res.Body)
+	body, err := io.ReadAll(res.Body)
 	if err != nil {
 		return err
 	}
diff --git a/providers/line/line.go b/providers/line/line.go
index e4d2e95..f3224af 100644
--- a/providers/line/line.go
+++ b/providers/line/line.go
@@ -6,9 +6,11 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
+	"time"
 
+	"github.com/golang-jwt/jwt/v4"
 	"github.com/markbates/goth"
 	"golang.org/x/oauth2"
 )
@@ -17,8 +19,14 @@ const (
 	authURL      string = "https://access.line.me/oauth2/v2.1/authorize"
 	tokenURL     string = "https://api.line.me/oauth2/v2.1/token"
 	endpointUser string = "https://api.line.me/v2/profile"
+	issuerURL    string = "https://access.line.me"
 )
 
+type IDTokenClaims struct {
+	jwt.StandardClaims
+	Email string `json:"email"`
+}
+
 // Provider is the implementation of `goth.Provider` for accessing Line.me.
 type Provider struct {
 	ClientKey       string
@@ -95,17 +103,15 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 
 	response, err := c.Do(req)
 	if err != nil {
-		if response != nil {
-			response.Body.Close()
-		}
 		return user, err
 	}
+	defer response.Body.Close()
 
 	if response.StatusCode != http.StatusOK {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
@@ -125,6 +131,13 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 	user.NickName = u.DisplayName
 	user.AvatarURL = u.PictureURL
 	user.UserID = u.UserID
+
+	if sess.IDToken != "" {
+		if err = p.addDataFromIdToken(sess.IDToken, &user); err != nil {
+			return user, err
+		}
+	}
+
 	return user, err
 }
 
@@ -141,9 +154,7 @@ func newConfig(provider *Provider, scopes []string) *oauth2.Config {
 	}
 
 	if len(scopes) > 0 {
-		for _, scope := range scopes {
-			c.Scopes = append(c.Scopes, scope)
-		}
+		c.Scopes = append(c.Scopes, scopes...)
 	}
 	return c
 }
@@ -167,3 +178,36 @@ func (p *Provider) SetBotPrompt(botPrompt string) {
 	}
 	p.authCodeOptions = append(p.authCodeOptions, oauth2.SetAuthURLParam("bot_prompt", botPrompt))
 }
+
+func (p *Provider) addDataFromIdToken(idToken string, user *goth.User) error {
+	token, err := jwt.ParseWithClaims(idToken, &IDTokenClaims{}, func(t *jwt.Token) (interface{}, error) {
+		claims := t.Claims.(*IDTokenClaims)
+		vErr := new(jwt.ValidationError)
+
+		if !claims.VerifyAudience(p.ClientKey, true) {
+			vErr.Inner = fmt.Errorf("audience is incorrect")
+			vErr.Errors |= jwt.ValidationErrorAudience
+		}
+		if !claims.VerifyIssuer(issuerURL, true) {
+			vErr.Inner = fmt.Errorf("issuer is incorrect")
+			vErr.Errors |= jwt.ValidationErrorIssuer
+		}
+		if !claims.VerifyExpiresAt(time.Now().Unix(), true) {
+			vErr.Inner = fmt.Errorf("token is expired")
+			vErr.Errors |= jwt.ValidationErrorExpired
+		}
+		if vErr.Errors > 0 {
+			return nil, vErr
+		}
+
+		return []byte(p.Secret), nil
+	})
+
+	if err != nil {
+		return err
+	}
+
+	user.Email = token.Claims.(*IDTokenClaims).Email
+
+	return nil
+}
diff --git a/providers/line/session.go b/providers/line/session.go
index 158a2dd..0213bc1 100644
--- a/providers/line/session.go
+++ b/providers/line/session.go
@@ -15,6 +15,7 @@ type Session struct {
 	AccessToken  string
 	RefreshToken string
 	ExpiresAt    time.Time
+	IDToken      string
 }
 
 var _ goth.Session = &Session{}
@@ -42,6 +43,11 @@ func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string,
 	s.AccessToken = token.AccessToken
 	s.RefreshToken = token.RefreshToken
 	s.ExpiresAt = token.Expiry
+
+	if idToken := token.Extra("id_token"); idToken != nil {
+		s.IDToken = idToken.(string)
+	}
+
 	return token.AccessToken, err
 }
 
diff --git a/providers/mailru/mailru.go b/providers/mailru/mailru.go
index db38280..f1d15ea 100644
--- a/providers/mailru/mailru.go
+++ b/providers/mailru/mailru.go
@@ -5,7 +5,7 @@ package mailru
 import (
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -101,7 +101,7 @@ func (p *Provider) FetchUser(session goth.Session) (_ goth.User, err error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.name, res.StatusCode)
 	}
 
-	buf, err := ioutil.ReadAll(res.Body)
+	buf, err := io.ReadAll(res.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/mastodon/mastodon.go b/providers/mastodon/mastodon.go
index a69788d..58c018c 100644
--- a/providers/mastodon/mastodon.go
+++ b/providers/mastodon/mastodon.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"strings"
 
@@ -112,7 +111,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/meetup/meetup.go b/providers/meetup/meetup.go
index 3d1ef0c..94aa053 100644
--- a/providers/meetup/meetup.go
+++ b/providers/meetup/meetup.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"strconv"
 
@@ -104,7 +103,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/naver/naver.go b/providers/naver/naver.go
index dad6beb..2ebce63 100644
--- a/providers/naver/naver.go
+++ b/providers/naver/naver.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -72,7 +71,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/nextcloud/README.md b/providers/nextcloud/README.md
index 841668f..ae7d566 100644
--- a/providers/nextcloud/README.md
+++ b/providers/nextcloud/README.md
@@ -6,7 +6,7 @@ on your own private server.
 ## Setting up Nextcloud Test Environment
 
 To test, you only need a working Docker image of Nextcloud running on a public
-URL, e.g. through [traefik](https://traefik.io/)
+URL, e.g. through [traefik](https://traefik.io)
 
 ```docker-compose.yml
 version: '2'
diff --git a/providers/nextcloud/nextcloud.go b/providers/nextcloud/nextcloud.go
index 23d3ee8..43bc859 100644
--- a/providers/nextcloud/nextcloud.go
+++ b/providers/nextcloud/nextcloud.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -132,7 +131,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/okta/okta.go b/providers/okta/okta.go
index b53eae2..b871b3d 100644
--- a/providers/okta/okta.go
+++ b/providers/okta/okta.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -109,7 +108,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/onedrive/onedrive.go b/providers/onedrive/onedrive.go
index dcb96e9..877894f 100644
--- a/providers/onedrive/onedrive.go
+++ b/providers/onedrive/onedrive.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 
@@ -94,7 +93,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/openidConnect/openidConnect.go b/providers/openidConnect/openidConnect.go
index 4a72159..c928547 100644
--- a/providers/openidConnect/openidConnect.go
+++ b/providers/openidConnect/openidConnect.go
@@ -6,7 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/url"
 	"strings"
@@ -51,13 +51,14 @@ const (
 
 // Provider is the implementation of `goth.Provider` for accessing OpenID Connect provider
 type Provider struct {
-	ClientKey    string
-	Secret       string
-	CallbackURL  string
-	HTTPClient   *http.Client
-	OpenIDConfig *OpenIDConfig
-	config       *oauth2.Config
-	providerName string
+	ClientKey       string
+	Secret          string
+	CallbackURL     string
+	HTTPClient      *http.Client
+	OpenIDConfig    *OpenIDConfig
+	config          *oauth2.Config
+	authCodeOptions []oauth2.AuthCodeOption
+	providerName    string
 
 	UserIdClaims    []string
 	NameClaims      []string
@@ -186,6 +187,14 @@ func (p *Provider) SetName(name string) {
 	p.providerName = name
 }
 
+// SetAuthCodeOptions sets additional parameters for the authentication URL.
+// It takes a map of string key-value pairs and appends them to the provider's authCodeOptions.
+func (p *Provider) SetAuthCodeOptions(params map[string]string) {
+	for k, v := range params {
+		p.authCodeOptions = append(p.authCodeOptions, oauth2.SetAuthURLParam(k, v))
+	}
+}
+
 func (p *Provider) Client() *http.Client {
 	return goth.HTTPClientWithFallBack(p.HTTPClient)
 }
@@ -195,7 +204,7 @@ func (p *Provider) Debug(debug bool) {}
 
 // BeginAuth asks the OpenID Connect provider for an authentication end-point.
 func (p *Provider) BeginAuth(state string) (goth.Session, error) {
-	url := p.config.AuthCodeURL(state)
+	url := p.config.AuthCodeURL(state, p.authCodeOptions...)
 	session := &Session{
 		AuthURL: url,
 	}
@@ -288,7 +297,7 @@ func (p *Provider) RefreshTokenWithIDToken(refreshToken string) (*RefreshTokenRe
 		return nil, fmt.Errorf("Non-200 response from RefreshToken: %d, WWW-Authenticate=%s", resp.StatusCode, resp.Header.Get("WWW-Authenticate"))
 	}
 
-	body, err := ioutil.ReadAll(resp.Body)
+	body, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return nil, err
 	}
@@ -401,7 +410,7 @@ func (p *Provider) fetchUserInfo(url, accessToken string) (map[string]interface{
 
 	// The UserInfo Claims MUST be returned as the members of a JSON object
 	// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
-	data, err := ioutil.ReadAll(resp.Body)
+	data, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return nil, err
 	}
@@ -420,7 +429,7 @@ func getOpenIDConfig(p *Provider, openIDAutoDiscoveryURL string) (*OpenIDConfig,
 		return nil, fmt.Errorf("Non-success code for Discovery URL: %d", res.StatusCode)
 	}
 
-	body, err := ioutil.ReadAll(res.Body)
+	body, err := io.ReadAll(res.Body)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/patreon/patreon.go b/providers/patreon/patreon.go
index a599b75..b960aa2 100644
--- a/providers/patreon/patreon.go
+++ b/providers/patreon/patreon.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"time"
 
@@ -143,7 +142,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/paypal/paypal.go b/providers/paypal/paypal.go
index bca0b8b..64579f6 100644
--- a/providers/paypal/paypal.go
+++ b/providers/paypal/paypal.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"os"
@@ -126,7 +125,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/reddit/reddit.go b/providers/reddit/reddit.go
index 2843c2f..f3d3285 100644
--- a/providers/reddit/reddit.go
+++ b/providers/reddit/reddit.go
@@ -3,11 +3,12 @@ package reddit
 import (
 	"encoding/json"
 	"fmt"
-	"github.com/markbates/goth"
-	"golang.org/x/oauth2"
 	"io"
 	"net/http"
 	"time"
+
+	"github.com/markbates/goth"
+	"golang.org/x/oauth2"
 )
 
 const (
diff --git a/providers/seatalk/seatalk.go b/providers/seatalk/seatalk.go
index fd6161b..e399eb4 100644
--- a/providers/seatalk/seatalk.go
+++ b/providers/seatalk/seatalk.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/url"
 	"strings"
@@ -101,7 +101,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	responseBytes, err := ioutil.ReadAll(response.Body)
+	responseBytes, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/slack/slack.go b/providers/slack/slack.go
index 84036ea..daec6f4 100644
--- a/providers/slack/slack.go
+++ b/providers/slack/slack.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -104,7 +103,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
@@ -130,7 +129,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 			return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 		}
 
-		bits, err = ioutil.ReadAll(response.Body)
+		bits, err = io.ReadAll(response.Body)
 		if err != nil {
 			return user, err
 		}
diff --git a/providers/soundcloud/soundcloud.go b/providers/soundcloud/soundcloud.go
index 2e11ee6..5e6dff7 100644
--- a/providers/soundcloud/soundcloud.go
+++ b/providers/soundcloud/soundcloud.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -99,7 +98,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/steam/session.go b/providers/steam/session.go
index 8f60dc2..7f06c8c 100644
--- a/providers/steam/session.go
+++ b/providers/steam/session.go
@@ -4,7 +4,7 @@ package steam
 import (
 	"encoding/json"
 	"errors"
-	"io/ioutil"
+	"io"
 	"net/url"
 	"regexp"
 	"strings"
@@ -56,7 +56,7 @@ func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string,
 		return "", err
 	}
 	defer resp.Body.Close()
-	content, err := ioutil.ReadAll(resp.Body)
+	content, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return "", err
 	}
diff --git a/providers/strava/strava.go b/providers/strava/strava.go
index 785cc7f..4527844 100644
--- a/providers/strava/strava.go
+++ b/providers/strava/strava.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
@@ -101,7 +100,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/tiktok/session.go b/providers/tiktok/session.go
index 2430b17..ff917c8 100644
--- a/providers/tiktok/session.go
+++ b/providers/tiktok/session.go
@@ -3,7 +3,7 @@ package tiktok
 import (
 	"encoding/json"
 	"errors"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/url"
 	"time"
@@ -68,7 +68,7 @@ func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string,
 	}{}
 
 	// Get the body bytes in case we have to parse an error response
-	bodyBytes, err := ioutil.ReadAll(response.Body)
+	bodyBytes, err := io.ReadAll(response.Body)
 	if err != nil {
 		return "", err
 	}
diff --git a/providers/tiktok/tiktok.go b/providers/tiktok/tiktok.go
index 1d33321..01066b7 100644
--- a/providers/tiktok/tiktok.go
+++ b/providers/tiktok/tiktok.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
@@ -143,7 +142,7 @@ func userFromReader(reader io.Reader, user *goth.User) error {
 		} `json:"data"`
 	}{}
 
-	bodyBytes, err := ioutil.ReadAll(reader)
+	bodyBytes, err := io.ReadAll(reader)
 	if err != nil {
 		return err
 	}
@@ -207,7 +206,7 @@ func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
 	}
 
 	// We get the body bytes in case we need to parse an error response
-	bodyBytes, err := ioutil.ReadAll(refreshResponse.Body)
+	bodyBytes, err := io.ReadAll(refreshResponse.Body)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/twitter/twitter.go b/providers/twitter/twitter.go
index 4e2e2c9..ad4abce 100644
--- a/providers/twitter/twitter.go
+++ b/providers/twitter/twitter.go
@@ -7,7 +7,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -118,7 +118,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/twitterv2/twitterv2.go b/providers/twitterv2/twitterv2.go
index 1dcb212..ee04ca7 100644
--- a/providers/twitterv2/twitterv2.go
+++ b/providers/twitterv2/twitterv2.go
@@ -7,7 +7,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -118,7 +118,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/typetalk/typetalk.go b/providers/typetalk/typetalk.go
index 73aa755..9822cb4 100644
--- a/providers/typetalk/typetalk.go
+++ b/providers/typetalk/typetalk.go
@@ -9,7 +9,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strconv"
@@ -103,7 +102,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user name", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
@@ -133,7 +132,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch profile", p.providerName, response.StatusCode)
 	}
 
-	bits, err = ioutil.ReadAll(response.Body)
+	bits, err = io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/vk/vk.go b/providers/vk/vk.go
index 615c81f..f0619f2 100644
--- a/providers/vk/vk.go
+++ b/providers/vk/vk.go
@@ -8,7 +8,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"strconv"
 
@@ -98,7 +97,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/xero/xero.go b/providers/xero/xero.go
index bb642c3..621bd6b 100644
--- a/providers/xero/xero.go
+++ b/providers/xero/xero.go
@@ -7,7 +7,7 @@ import (
 	"encoding/pem"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"log"
 	"net/http"
 	"os"
@@ -155,7 +155,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 	}
 
 	var apiResponse APIResponse
-	responseBytes, err := ioutil.ReadAll(response.Body)
+	responseBytes, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, fmt.Errorf("Could not read response: %s", err.Error())
 	}
@@ -204,7 +204,7 @@ func newPublicConsumer(provider *Provider, authURL string) *oauth.Consumer {
 
 // newPartnerConsumer creates a consumer capable of communicating with a Partner application: https://developer.xero.com/documentation/auth-and-limits/partner-applications
 func newPrivateOrPartnerConsumer(provider *Provider, authURL string) *oauth.Consumer {
-	privateKeyFileContents, err := ioutil.ReadFile(privateKeyFilePath)
+	privateKeyFileContents, err := os.ReadFile(privateKeyFilePath)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/providers/yahoo/yahoo.go b/providers/yahoo/yahoo.go
index 0dd13ab..e6a2065 100644
--- a/providers/yahoo/yahoo.go
+++ b/providers/yahoo/yahoo.go
@@ -15,7 +15,7 @@ import (
 const (
 	authURL         string = "https://api.login.yahoo.com/oauth2/request_auth"
 	tokenURL        string = "https://api.login.yahoo.com/oauth2/get_token"
-	endpointProfile string = "https://social.yahooapis.com/v1/user/GUID/profile?format=json"
+	endpointProfile string = "https://api.login.yahoo.com/openid/v1/userinfo"
 )
 
 // Provider is the implementation of `goth.Provider` for accessing Yahoo.
@@ -123,27 +123,29 @@ func newConfig(provider *Provider, scopes []string) *oauth2.Config {
 	return c
 }
 
+type yahooUser struct {
+	Email      string `json:"email"`
+	Name       string `json:"name"`
+	GivenName  string `json:"given_name"`
+	FamilyName string `json:"family_name"`
+	Nickname   string `json:"nickname"`
+	Picture    string `json:"picture"`
+	Sub        string `json:"sub"`
+}
+
 func userFromReader(r io.Reader, user *goth.User) error {
-	u := struct {
-		Profile struct {
-			NickName string `json:"nickname"`
-			Location string `json:"location"`
-			ID       string `json:"guid"`
-			Image    struct {
-				ImageURL string `json:"imageURL"`
-			} `json:"image"`
-		} `json:"profile"`
-	}{}
+	u := yahooUser{}
 	err := json.NewDecoder(r).Decode(&u)
 	if err != nil {
 		return err
 	}
-	user.Email = "" // email is not provided by yahoo
-	user.Name = u.Profile.NickName
-	user.NickName = u.Profile.NickName
-	user.UserID = u.Profile.ID
-	user.Location = u.Profile.Location
-	user.AvatarURL = u.Profile.Image.ImageURL
+	user.Email = u.Email
+	user.Name = u.Name
+	user.FirstName = u.GivenName
+	user.LastName = u.FamilyName
+	user.NickName = u.Nickname
+	user.AvatarURL = u.Picture
+	user.UserID = u.Sub
 	return nil
 }
 
diff --git a/providers/yammer/session.go b/providers/yammer/session.go
index cd8fe78..b39019e 100644
--- a/providers/yammer/session.go
+++ b/providers/yammer/session.go
@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 	"strings"
@@ -76,7 +75,7 @@ func retrieveAuthData(p *Provider, TokenURL string, v url.Values) (map[string]ma
 		return nil, err
 	}
 	defer r.Body.Close()
-	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
+	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
 	if err != nil {
 		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
 	}
diff --git a/providers/yammer/yammer.go b/providers/yammer/yammer.go
index f7e803e..bcc7a21 100644
--- a/providers/yammer/yammer.go
+++ b/providers/yammer/yammer.go
@@ -7,7 +7,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"strconv"
 
@@ -99,7 +99,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(response.Body)
+	bits, err := io.ReadAll(response.Body)
 	if err != nil {
 		return user, err
 	}
diff --git a/providers/yandex/yandex.go b/providers/yandex/yandex.go
index e550a80..a500564 100644
--- a/providers/yandex/yandex.go
+++ b/providers/yandex/yandex.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 
 	"github.com/markbates/goth"
@@ -103,7 +102,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
 	}
 
-	bits, err := ioutil.ReadAll(resp.Body)
+	bits, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return user, err
 	}


Test patch:
diff --git a/gothic/gothic_test.go b/gothic/gothic_test.go
index bb0254a..22c8448 100644
--- a/gothic/gothic_test.go
+++ b/gothic/gothic_test.go
@@ -5,7 +5,7 @@ import (
 	"compress/gzip"
 	"fmt"
 	"html"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -283,7 +283,7 @@ func ungzipString(value string) string {
 	if err != nil {
 		return "err"
 	}
-	s, err := ioutil.ReadAll(r)
+	s, err := io.ReadAll(r)
 	if err != nil {
 		return "err"
 	}
diff --git a/gothic/provider_test.go b/gothic/provider_test.go
new file mode 100644
index 0000000..bf2b456
--- /dev/null
+++ b/gothic/provider_test.go
@@ -0,0 +1,47 @@
+//go:build go1.22
+// +build go1.22
+
+package gothic_test
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+
+	"github.com/markbates/goth/gothic"
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_GetAuthURL122(t *testing.T) {
+	a := assert.New(t)
+
+	res := httptest.NewRecorder()
+	req, err := http.NewRequest("GET", "/auth", nil)
+	a.NoError(err)
+	req.SetPathValue("provider", "faux")
+
+	u, err := gothic.GetAuthURL(res, req)
+	a.NoError(err)
+
+	// Check that we get the correct auth URL with a state parameter
+	parsed, err := url.Parse(u)
+	a.NoError(err)
+	a.Equal("http", parsed.Scheme)
+	a.Equal("example.com", parsed.Host)
+	q := parsed.Query()
+	a.Contains(q, "client_id")
+	a.Equal("code", q.Get("response_type"))
+	a.NotZero(q, "state")
+
+	// Check that if we run GetAuthURL on another request, that request's
+	// auth URL has a different state from the previous one.
+	req2, err := http.NewRequest("GET", "/auth?provider=faux", nil)
+	a.NoError(err)
+	req2.SetPathValue("provider", "faux")
+	url2, err := gothic.GetAuthURL(httptest.NewRecorder(), req2)
+	a.NoError(err)
+	parsed2, err := url.Parse(url2)
+	a.NoError(err)
+	a.NotEqual(parsed.Query().Get("state"), parsed2.Query().Get("state"))
+}
diff --git a/providers/kakao/session_test.go b/providers/kakao/session_test.go
index bf17a6a..90d3f3c 100644
--- a/providers/kakao/session_test.go
+++ b/providers/kakao/session_test.go
@@ -36,7 +36,7 @@ func Test_ToJSON(t *testing.T) {
 	s := &line.Session{}
 
 	data := s.Marshal()
-	a.Equal(data, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z"}`)
+	a.Equal(`{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z","IDToken":""}`, data)
 }
 
 func Test_String(t *testing.T) {
diff --git a/providers/lark/lark_test.go b/providers/lark/lark_test.go
new file mode 100644
index 0000000..cda49e5
--- /dev/null
+++ b/providers/lark/lark_test.go
@@ -0,0 +1,185 @@
+package lark_test
+
+import (
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/markbates/goth/providers/lark"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+type MockedHTTPClient struct {
+	mock.Mock
+}
+
+func (m *MockedHTTPClient) RoundTrip(req *http.Request) (*http.Response, error) {
+	args := m.Mock.Called(req)
+	return args.Get(0).(*http.Response), args.Error(1)
+}
+
+func Test_New(t *testing.T) {
+	t.Parallel()
+	a := assert.New(t)
+	p := larkProvider()
+
+	a.Equal(p.ClientKey, os.Getenv("LARK_APP_ID"))
+	a.Equal(p.Secret, os.Getenv("LARK_APP_SECRET"))
+	a.Equal(p.CallbackURL, "/foo")
+}
+
+func Test_BeginAuth(t *testing.T) {
+	t.Parallel()
+	a := assert.New(t)
+	p := larkProvider()
+	session, err := p.BeginAuth("test_state")
+	s := session.(*lark.Session)
+	a.NoError(err)
+	a.Contains(s.AuthURL, "https://open.feishu.cn/open-apis/authen/v1/authorize")
+	a.Contains(s.AuthURL, "app_id="+os.Getenv("LARK_APP_ID"))
+	a.Contains(s.AuthURL, "state=test_state")
+	a.Contains(s.AuthURL, fmt.Sprintf("redirect_uri=%s", url.QueryEscape("/foo")))
+}
+
+func Test_GetAppAccessToken(t *testing.T) {
+	t.Run("happy path", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusOK,
+			Body:       ioutil.NopCloser(strings.NewReader(`{"code":0,"msg":"ok","app_access_token":"test_token","expire":3600}`)),
+		}, nil)
+
+		err := p.GetAppAccessToken()
+		assert.NoError(t, err)
+	})
+
+	t.Run("error on request", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{}, errors.New("request error"))
+
+		err := p.GetAppAccessToken()
+		assert.Error(t, err)
+	})
+
+	t.Run("non-200 status code", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusForbidden,
+			Body:       ioutil.NopCloser(strings.NewReader(``)),
+		}, nil)
+
+		err := p.GetAppAccessToken()
+		assert.Error(t, err)
+	})
+
+	t.Run("error on response decode", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusOK,
+			Body:       ioutil.NopCloser(strings.NewReader(`not a json`)),
+		}, nil)
+
+		err := p.GetAppAccessToken()
+		assert.Error(t, err)
+	})
+
+	t.Run("error code in response", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusOK,
+			Body:       ioutil.NopCloser(strings.NewReader(`{"code":1,"msg":"error message"}`)),
+		}, nil)
+
+		err := p.GetAppAccessToken()
+		assert.Error(t, err)
+	})
+}
+
+func Test_FetchUser(t *testing.T) {
+	session := &lark.Session{
+		AccessToken: "user_access_token",
+	}
+
+	t.Run("happy path", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusOK,
+			Body:       ioutil.NopCloser(strings.NewReader(`{"code":0,"msg":"ok","data":{"user_id":"test_user_id","name":"test_name","avatar_url":"test_avatar_url","enterprise_email":"test_email"}}`)),
+		}, nil)
+		user, err := p.FetchUser(session)
+		require.NoError(t, err)
+		assert.Equal(t, user.UserID, "test_user_id")
+		assert.Equal(t, user.Name, "test_name")
+		assert.Equal(t, user.AvatarURL, "test_avatar_url")
+		assert.Equal(t, user.Email, "test_email")
+	})
+	t.Run("error on request", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{}, errors.New("request error"))
+		_, err := p.FetchUser(session)
+		require.Error(t, err)
+	})
+	t.Run("non-200 status code", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusForbidden,
+			Body:       ioutil.NopCloser(strings.NewReader(``)),
+		}, nil)
+		_, err := p.FetchUser(session)
+		require.Error(t, err)
+	})
+	t.Run("error on response decode", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusOK,
+			Body:       ioutil.NopCloser(strings.NewReader(`not a json`)),
+		}, nil)
+		_, err := p.FetchUser(session)
+		require.Error(t, err)
+	})
+	t.Run("error code in response", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusOK,
+			Body:       ioutil.NopCloser(strings.NewReader(`{"code":1,"msg":"error message"}`)),
+		}, nil)
+		_, err := p.FetchUser(session)
+		require.Error(t, err)
+	})
+}
+
+func larkProvider() *lark.Provider {
+	return lark.New(os.Getenv("LARK_APP_ID"), os.Getenv("LARK_APP_SECRET"), "/foo")
+}
diff --git a/providers/lark/session_test.go b/providers/lark/session_test.go
new file mode 100644
index 0000000..59dc53f
--- /dev/null
+++ b/providers/lark/session_test.go
@@ -0,0 +1,112 @@
+package lark_test
+
+import (
+	"errors"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"testing"
+
+	"github.com/markbates/goth"
+	"github.com/markbates/goth/providers/lark"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+type MockParams struct {
+	params map[string]string
+}
+
+func (m *MockParams) Get(key string) string {
+	return m.params[key]
+}
+
+func Test_Implements_Session(t *testing.T) {
+	t.Parallel()
+	a := assert.New(t)
+	s := &lark.Session{}
+
+	a.Implements((*goth.Session)(nil), s)
+}
+
+func Test_GetAuthURL(t *testing.T) {
+	t.Run("happy path", func(t *testing.T) {
+		session := &lark.Session{
+			AuthURL: "https://auth.url",
+		}
+		url, err := session.GetAuthURL()
+		assert.NoError(t, err)
+		assert.Equal(t, "https://auth.url", url)
+	})
+
+	t.Run("missing AuthURL", func(t *testing.T) {
+		session := &lark.Session{}
+		_, err := session.GetAuthURL()
+		assert.Error(t, err)
+	})
+}
+
+func Test_Marshal(t *testing.T) {
+	session := &lark.Session{
+		AuthURL:     "https://auth.url",
+		AccessToken: "access_token",
+	}
+	marshaled := session.Marshal()
+	assert.Contains(t, marshaled, "https://auth.url")
+	assert.Contains(t, marshaled, "access_token")
+}
+
+func Test_Authorize(t *testing.T) {
+	session := &lark.Session{}
+	params := &MockParams{
+		params: map[string]string{
+			"code": "authorization_code",
+		},
+	}
+
+	t.Run("error on request", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{}, errors.New("request error"))
+		_, err := session.Authorize(p, params)
+		require.Error(t, err)
+	})
+
+	t.Run("non-200 status code", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusForbidden,
+			Body:       ioutil.NopCloser(strings.NewReader(``)),
+		}, nil)
+		_, err := session.Authorize(p, params)
+		require.Error(t, err)
+	})
+
+	t.Run("error on response decode", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusOK,
+			Body:       ioutil.NopCloser(strings.NewReader(`not a json`)),
+		}, nil)
+		_, err := session.Authorize(p, params)
+		require.Error(t, err)
+	})
+
+	t.Run("error code in response", func(t *testing.T) {
+		mockClient := new(MockedHTTPClient)
+		p := larkProvider()
+		p.HTTPClient = &http.Client{Transport: mockClient}
+		mockClient.On("RoundTrip", mock.Anything).Return(&http.Response{
+			StatusCode: http.StatusOK,
+			Body:       ioutil.NopCloser(strings.NewReader(`{"code":1,"msg":"error message"}`)),
+		}, nil)
+		_, err := session.Authorize(p, params)
+		require.Error(t, err)
+	})
+}
diff --git a/providers/line/session_test.go b/providers/line/session_test.go
index 719bbb1..827565d 100644
--- a/providers/line/session_test.go
+++ b/providers/line/session_test.go
@@ -36,7 +36,7 @@ func Test_ToJSON(t *testing.T) {
 	s := &line.Session{}
 
 	data := s.Marshal()
-	a.Equal(data, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z"}`)
+	a.Equal(data, `{"AuthURL":"","AccessToken":"","RefreshToken":"","ExpiresAt":"0001-01-01T00:00:00Z","IDToken":""}`)
 }
 
 func Test_String(t *testing.T) {
diff --git a/providers/openidConnect/openidConnect_test.go b/providers/openidConnect/openidConnect_test.go
index 3e84435..7dd76e0 100644
--- a/providers/openidConnect/openidConnect_test.go
+++ b/providers/openidConnect/openidConnect_test.go
@@ -78,6 +78,24 @@ func Test_BeginAuth(t *testing.T) {
 	a.Contains(s.AuthURL, "scope=openid")
 }
 
+func Test_BeginAuth_AuthCodeOptions(t *testing.T) {
+	t.Parallel()
+	a := assert.New(t)
+
+	provider := openidConnectProvider()
+	provider.SetAuthCodeOptions(map[string]string{"domain_hint": "test_domain.com", "prompt": "none"})
+	session, err := provider.BeginAuth("test_state")
+	s := session.(*Session)
+	a.NoError(err)
+	a.Contains(s.AuthURL, "https://accounts.google.com/o/oauth2/v2/auth")
+	a.Contains(s.AuthURL, fmt.Sprintf("client_id=%s", os.Getenv("OPENID_CONNECT_KEY")))
+	a.Contains(s.AuthURL, "state=test_state")
+	a.Contains(s.AuthURL, "redirect_uri=http%3A%2F%2Flocalhost%2Ffoo")
+	a.Contains(s.AuthURL, "scope=openid")
+	a.Contains(s.AuthURL, "domain_hint=test_domain.com")
+	a.Contains(s.AuthURL, "prompt=none")
+}
+
 func Test_Implements_Provider(t *testing.T) {
 	t.Parallel()
 	a := assert.New(t)