├── .gitignore ├── go.mod ├── constant.go ├── go.sum ├── context.go ├── jwt_test.go ├── LICENSE ├── logger.go ├── examples ├── client │ └── main.go ├── go.mod ├── server │ └── main.go └── go.sum ├── keys.go ├── utils.go ├── options.go ├── models.go ├── errors.go ├── func.go ├── jwt.go ├── README.md ├── client.go └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea/ -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nilorg/oauth2 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/go-jose/go-jose/v4 v4.1.3 9 | github.com/nilorg/pkg v0.0.0-20251216130642-faa5836f8085 10 | github.com/nilorg/sdk v0.0.0-20251216121711-27713f2bd609 11 | ) 12 | 13 | require github.com/deckarep/golang-set v1.8.0 // indirect 14 | -------------------------------------------------------------------------------- /constant.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import "time" 4 | 5 | const ( 6 | // contentTypeJSON JSON内容类型 / JSON content type for HTTP responses 7 | contentTypeJSON = "application/json" 8 | // AccessTokenExpire 访问令牌过期时间(1小时) / Access token expiration time (1 hour) 9 | AccessTokenExpire = time.Second * 3600 10 | // RefreshTokenExpire 刷新令牌过期时间(30分钟) / Refresh token expiration time (30 minutes) 11 | RefreshTokenExpire = AccessTokenExpire / 2 12 | // TokenTypeBearer Bearer令牌类型 / Bearer token type 13 | TokenTypeBearer = "Bearer" 14 | // ScopeRefreshToken 刷新令牌的scope / Scope for refresh token 15 | ScopeRefreshToken = "refresh_token" 16 | // DefaultJwtIssuer 默认JWT颁发者 / Default JWT issuer 17 | DefaultJwtIssuer = "github.com/nilorg/oauth2" 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= 2 | github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= 3 | github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= 4 | github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 5 | github.com/nilorg/pkg v0.0.0-20251216130642-faa5836f8085 h1:/Q6lKH6HXrnoY/ftjoLJwlGmbp/MXtrYhlXisr42OWs= 6 | github.com/nilorg/pkg v0.0.0-20251216130642-faa5836f8085/go.mod h1:gd0jjR4fn+9qFO47NEvFKdlsEUWJ0OclO1ex59Vh41s= 7 | github.com/nilorg/sdk v0.0.0-20251216121711-27713f2bd609 h1:J0V/I+2AuWIrF5ZTyPlQEsP6VXmH8aTtJCWxtPh6GOM= 8 | github.com/nilorg/sdk v0.0.0-20251216121711-27713f2bd609/go.mod h1:X1swpPdqguAZaBDoEPyEWHSsJii0YQ1o+3piMv6W3JU= 9 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | // openIDKey OpenID上下文键类型 / Context key type for OpenID 9 | type openIDKey struct{} 10 | 11 | var ( 12 | // ErrContextNotFoundOpenID 上下文不存在OpenID / OpenID not found in context 13 | ErrContextNotFoundOpenID = errors.New("openid not found in context") 14 | ) 15 | 16 | // OpenIDFromContext 从上下文中获取OpenID / Get OpenID from context 17 | func OpenIDFromContext(ctx context.Context) (string, error) { 18 | openID, ok := ctx.Value(openIDKey{}).(string) 19 | if !ok { 20 | return "", ErrContextNotFoundOpenID 21 | } 22 | return openID, nil 23 | } 24 | 25 | // NewOpenIDContext 创建包含OpenID的上下文 / Create context with OpenID 26 | func NewOpenIDContext(ctx context.Context, openID string) context.Context { 27 | return context.WithValue(ctx, openIDKey{}, openID) 28 | } 29 | -------------------------------------------------------------------------------- /jwt_test.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestGenerateToken(t *testing.T) { 9 | cl := JwtClaims{ 10 | JwtStandardClaims: JwtStandardClaims{ 11 | Subject: "subject", 12 | Issuer: "http://localhost:8080", 13 | NotBefore: time.Now().Unix(), 14 | Audience: []string{ 15 | "oauth2-client-test", 16 | }, 17 | ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), 18 | }, 19 | } 20 | token, err := NewHS256JwtClaimsToken(&cl, []byte("test")) 21 | if err != nil { 22 | t.Error(err) 23 | return 24 | } 25 | t.Logf("token: %s", token) 26 | } 27 | 28 | func TestParseJwtToken(t *testing.T) { 29 | tokenClaims, err := ParseHS256JwtClaimsToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyLWNsaWVudC10ZXN0Il0sImV4cCI6MTU4OTc4OTUyMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwIiwibmJmIjoxNTg5NzAzMTIwLCJzdWIiOiJzdWJqZWN0In0.D0h0tKcGf2t7FwE5tkxZ8zTLozUFHfteKFU6tuL3dWA", []byte("test")) 30 | if err != nil { 31 | t.Error(err) 32 | return 33 | } 34 | t.Logf("token: %+v", tokenClaims) 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nil Org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | // Logger logger 9 | type Logger interface { 10 | // Debugf 测试 11 | Debugf(ctx context.Context, format string, args ...interface{}) 12 | // Debugln 测试 13 | Debugln(ctx context.Context, args ...interface{}) 14 | // Errorf 错误 15 | Errorf(ctx context.Context, format string, args ...interface{}) 16 | // Errorln 错误 17 | Errorln(ctx context.Context, args ...interface{}) 18 | } 19 | 20 | // DefaultLogger ... 21 | type DefaultLogger struct{} 22 | 23 | // Debugf ... 24 | func (*DefaultLogger) Debugf(_ context.Context, format string, args ...interface{}) { 25 | fmt.Printf("OAuth2 [DEBUG] "+format+"\n", args...) 26 | } 27 | 28 | // Debugln ... 29 | func (*DefaultLogger) Debugln(_ context.Context, args ...interface{}) { 30 | fmt.Println("OAuth2 [DEBUG] ", args) 31 | } 32 | 33 | // Errorf ... 34 | func (*DefaultLogger) Errorf(_ context.Context, format string, args ...interface{}) { 35 | fmt.Printf("OAuth2 [ERROR] "+format+"\n", args...) 36 | } 37 | 38 | // Errorln ... 39 | func (*DefaultLogger) Errorln(_ context.Context, args ...interface{}) { 40 | fmt.Println("OAuth2 [ERROR] ", args) 41 | } 42 | -------------------------------------------------------------------------------- /examples/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/nilorg/oauth2" 6 | "github.com/nilorg/pkg/logger" 7 | ) 8 | 9 | var ( 10 | client *oauth2.Client 11 | ) 12 | 13 | func init() { 14 | logger.Init() 15 | client = oauth2.NewClient("http://localhost:8003", "oauth2_client", "password") 16 | client.Log = &oauth2.DefaultLogger{} 17 | } 18 | func main() { 19 | r := gin.Default() 20 | r.GET("/ping", func(c *gin.Context) { 21 | //err := client.AuthorizeImplicit(c.Writer, "http://localhost:8080/callback", "test", "aaaaa") 22 | //if err != nil { 23 | // logger.Errorln(err) 24 | // return 25 | //} 26 | err := client.AuthorizeAuthorizationCode(c.Request.Context(), c.Writer, "http://localhost:8080/callback", "test", "bbbbb") 27 | if err != nil { 28 | logger.Errorln(err) 29 | return 30 | } 31 | }) 32 | r.GET("/callback", func(c *gin.Context) { 33 | code := c.Query("code") 34 | token, err := client.TokenAuthorizationCode(c.Request.Context(), code, c.Request.URL.String(), client.ID) 35 | if err != nil { 36 | c.JSON(200, gin.H{ 37 | "message": "callback", 38 | "err": err.Error(), 39 | }) 40 | } else { 41 | c.JSON(200, gin.H{ 42 | "message": "callback", 43 | "token": token, 44 | }) 45 | } 46 | }) 47 | 48 | r.Run() // listen and serve on 0.0.0.0:8080 49 | } 50 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | const ( 4 | // ResponseTypeKey 响应类型 / Response type parameter key 5 | ResponseTypeKey = "response_type" 6 | // ClientIDKey 客户端ID / Client identifier parameter key 7 | ClientIDKey = "client_id" 8 | // ClientSecretKey 客户端密钥 / Client secret parameter key 9 | ClientSecretKey = "client_secret" 10 | // RedirectURIKey 重定向URI / Redirect URI parameter key 11 | RedirectURIKey = "redirect_uri" 12 | // ScopeKey 授权范围 / Scope parameter key 13 | ScopeKey = "scope" 14 | // StateKey 状态码,用于防止CSRF攻击 / State parameter key for CSRF protection 15 | StateKey = "state" 16 | // GrantTypeKey 授权类型 / Grant type parameter key 17 | GrantTypeKey = "grant_type" 18 | // CodeKey 授权码 / Authorization code parameter key 19 | CodeKey = "code" 20 | // TokenKey 令牌 / Token parameter key 21 | TokenKey = "token" 22 | // ErrorKey 错误信息 / Error parameter key 23 | ErrorKey = "error" 24 | // AccessTokenKey 访问令牌 / Access token parameter key 25 | AccessTokenKey = "access_token" 26 | // TokenTypeKey 令牌类型 / Token type parameter key 27 | TokenTypeKey = "token_type" 28 | // ClientCredentialsKey 客户端凭证模式 / Client credentials grant type 29 | ClientCredentialsKey = "client_credentials" 30 | // PasswordKey 密码模式 / Resource owner password credentials grant type 31 | PasswordKey = "password" 32 | // UsernameKey 用户名 / Username parameter key 33 | UsernameKey = "username" 34 | // RefreshTokenKey 刷新令牌 / Refresh token parameter key 35 | RefreshTokenKey = "refresh_token" 36 | // AuthorizationCodeKey 授权码模式 / Authorization code grant type 37 | AuthorizationCodeKey = "authorization_code" 38 | // DeviceCodeKey 设备码模式 / Device code grant type 39 | DeviceCodeKey = "device_code" 40 | // UrnIetfParamsOAuthGrantTypeDeviceCodeKey 设备码模式URN格式 / Device code grant type in URN format (RFC 8628) 41 | UrnIetfParamsOAuthGrantTypeDeviceCodeKey = "urn:ietf:params:oauth:grant-type:device_code" 42 | // TokenTypeHintKey 令牌类型提示 / Token type hint parameter key 43 | TokenTypeHintKey = "token_type_hint" 44 | // ImplicitKey 隐式授权模式 / Implicit grant type 45 | ImplicitKey = "implicit" 46 | ) 47 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nilorg/oauth2/examples 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.1 6 | 7 | require ( 8 | github.com/gin-gonic/gin v1.10.0 9 | github.com/nilorg/oauth2 v0.0.0 10 | github.com/nilorg/pkg v0.0.0-20251216130642-faa5836f8085 11 | ) 12 | 13 | require ( 14 | github.com/bytedance/sonic v1.11.6 // indirect 15 | github.com/bytedance/sonic/loader v0.1.1 // indirect 16 | github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect 17 | github.com/cloudwego/base64x v0.1.4 // indirect 18 | github.com/cloudwego/iasm v0.2.0 // indirect 19 | github.com/deckarep/golang-set v1.8.0 // indirect 20 | github.com/evalphobia/logrus_sentry v0.8.2 // indirect 21 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 22 | github.com/getsentry/raven-go v0.2.0 // indirect 23 | github.com/gin-contrib/sse v0.1.0 // indirect 24 | github.com/go-jose/go-jose/v4 v4.1.3 // indirect 25 | github.com/go-playground/locales v0.14.1 // indirect 26 | github.com/go-playground/universal-translator v0.18.1 // indirect 27 | github.com/go-playground/validator/v10 v10.20.0 // indirect 28 | github.com/goccy/go-json v0.10.2 // indirect 29 | github.com/google/uuid v1.6.0 // indirect 30 | github.com/json-iterator/go v1.1.12 // indirect 31 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 32 | github.com/kr/pretty v0.3.1 // indirect 33 | github.com/leodido/go-urn v1.4.0 // indirect 34 | github.com/mattn/go-isatty v0.0.20 // indirect 35 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 36 | github.com/modern-go/reflect2 v1.0.2 // indirect 37 | github.com/nilorg/sdk v0.0.0-20251216121711-27713f2bd609 // indirect 38 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 39 | github.com/pkg/errors v0.9.1 // indirect 40 | github.com/sirupsen/logrus v1.8.3 // indirect 41 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 42 | github.com/ugorji/go/codec v1.2.12 // indirect 43 | golang.org/x/arch v0.8.0 // indirect 44 | golang.org/x/crypto v0.45.0 // indirect 45 | golang.org/x/net v0.47.0 // indirect 46 | golang.org/x/sys v0.38.0 // indirect 47 | golang.org/x/text v0.31.0 // indirect 48 | google.golang.org/grpc v1.72.0 // indirect 49 | google.golang.org/protobuf v1.36.6 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | ) 52 | 53 | replace github.com/nilorg/oauth2 => ../ 54 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/nilorg/sdk/random" 11 | ) 12 | 13 | // RequestClientBasic 获取请求中的客户端信息 14 | func RequestClientBasic(r *http.Request) (basic *ClientBasic, err error) { 15 | username, password, ok := r.BasicAuth() 16 | if !ok { 17 | username = r.PostFormValue("client_id") 18 | password = r.PostFormValue("client_secret") 19 | if username == "" || password == "" { 20 | err = ErrInvalidClient 21 | return 22 | } 23 | } 24 | basic = &ClientBasic{ 25 | ID: username, 26 | Secret: password, 27 | } 28 | return 29 | } 30 | func writerJSON(w http.ResponseWriter, statusCode int, value interface{}) (err error) { 31 | w.WriteHeader(statusCode) 32 | w.Header().Set("Content-Type", contentTypeJSON) 33 | w.Header().Set("Cache-Control", "no-store") 34 | w.Header().Set("Pragma", "no-cache") 35 | err = json.NewEncoder(w).Encode(value) 36 | return 37 | } 38 | 39 | // WriterJSON 写入Json 40 | func WriterJSON(w http.ResponseWriter, value interface{}) { 41 | err := writerJSON(w, http.StatusOK, value) 42 | if err != nil { 43 | panic(err) 44 | } 45 | } 46 | 47 | // WriterError 写入Error 48 | func WriterError(w http.ResponseWriter, err error) { 49 | statusCode := http.StatusBadRequest 50 | if code, ok := ErrStatusCodes[err]; ok { 51 | statusCode = code 52 | } 53 | if werr := writerJSON(w, statusCode, &ErrorResponse{ 54 | Error: err.Error(), 55 | }); werr != nil { 56 | panic(werr) 57 | } 58 | } 59 | 60 | // RedirectSuccess 重定向成功 61 | func RedirectSuccess(w http.ResponseWriter, r *http.Request, redirectURI *url.URL, code string) { 62 | query := redirectURI.Query() 63 | query.Set(CodeKey, code) 64 | query.Set(StateKey, r.FormValue(StateKey)) 65 | redirectURI.RawQuery = query.Encode() 66 | http.Redirect(w, r, redirectURI.String(), http.StatusFound) 67 | } 68 | 69 | // RedirectError 重定向错误 70 | func RedirectError(w http.ResponseWriter, r *http.Request, redirectURI *url.URL, err error) { 71 | query := redirectURI.Query() 72 | query.Set(ErrorKey, err.Error()) 73 | query.Set(StateKey, r.FormValue(StateKey)) 74 | redirectURI.RawQuery = query.Encode() 75 | http.Redirect(w, r, redirectURI.String(), http.StatusFound) 76 | } 77 | 78 | // RandomState 随机State 79 | func RandomState() string { 80 | return random.AZaz09(6) 81 | } 82 | 83 | // RandomCode 随机Code 84 | func RandomCode() string { 85 | return random.AZaz09(12) 86 | } 87 | 88 | // RandomDeviceCode 随机DeviceCode 89 | func RandomDeviceCode() string { 90 | return random.AZaz09(32) 91 | } 92 | 93 | // RandomUserCode 随机用户code 94 | func RandomUserCode() string { 95 | return fmt.Sprintf("%s-%s", random.Number(3), random.Number(3)) 96 | } 97 | 98 | // StringSplit strings.Split 99 | func StringSplit(s, sep string) (results []string) { 100 | results = strings.Split(s, sep) 101 | if len(results) == 1 { 102 | if results[0] == "" { 103 | results = []string{} 104 | } 105 | } 106 | return 107 | } 108 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | // ServerOptions server可选参数列表 4 | type ServerOptions struct { 5 | Log Logger 6 | Issuer string 7 | DeviceAuthorizationEndpointEnabled bool // https://tools.ietf.org/html/rfc8628 8 | DeviceVerificationURI string // https://tools.ietf.org/html/rfc8628#section-3.2 9 | IntrospectEndpointEnabled bool // https://tools.ietf.org/html/rfc7662 10 | TokenRevocationEnabled bool // https://tools.ietf.org/html/rfc7009 11 | CustomGrantTypeEnabled bool // 自定义身份验证 12 | CustomGrantTypeAuthentication map[string]CustomGrantTypeAuthenticationFunc 13 | } 14 | 15 | // ServerOption 为可选参数赋值的函数 16 | type ServerOption func(*ServerOptions) 17 | 18 | // ServerLogger 设置服务器日志记录器 / Set server logger 19 | func ServerLogger(log Logger) ServerOption { 20 | return func(o *ServerOptions) { 21 | o.Log = log 22 | } 23 | } 24 | 25 | // ServerIssuer 设置JWT颁发者 / Set JWT issuer 26 | func ServerIssuer(issuer string) ServerOption { 27 | return func(o *ServerOptions) { 28 | o.Issuer = issuer 29 | } 30 | } 31 | 32 | // ServerDeviceAuthorizationEndpointEnabled 启用设备授权端点 / Enable device authorization endpoint (RFC 8628) 33 | func ServerDeviceAuthorizationEndpointEnabled(deviceAuthorizationEndpointEnabled bool) ServerOption { 34 | return func(o *ServerOptions) { 35 | o.DeviceAuthorizationEndpointEnabled = deviceAuthorizationEndpointEnabled 36 | } 37 | } 38 | 39 | // ServerDeviceVerificationURI 设置设备验证URI / Set device verification URI 40 | func ServerDeviceVerificationURI(deviceVerificationURI string) ServerOption { 41 | return func(o *ServerOptions) { 42 | o.DeviceVerificationURI = deviceVerificationURI 43 | } 44 | } 45 | 46 | // ServerIntrospectEndpointEnabled 启用令牌内省端点 / Enable token introspection endpoint (RFC 7662) 47 | func ServerIntrospectEndpointEnabled(introspectEndpointEnabled bool) ServerOption { 48 | return func(o *ServerOptions) { 49 | o.IntrospectEndpointEnabled = introspectEndpointEnabled 50 | } 51 | } 52 | 53 | // ServerTokenRevocationEnabled 启用令牌撤销端点 / Enable token revocation endpoint (RFC 7009) 54 | func ServerTokenRevocationEnabled(tokenRevocationEnabled bool) ServerOption { 55 | return func(o *ServerOptions) { 56 | o.TokenRevocationEnabled = tokenRevocationEnabled 57 | } 58 | } 59 | 60 | // ServerCustomGrantTypeEnabled 启用自定义授权类型 / Enable custom grant types 61 | func ServerCustomGrantTypeEnabled(customGrantTypeEnabled bool) ServerOption { 62 | return func(o *ServerOptions) { 63 | o.CustomGrantTypeEnabled = customGrantTypeEnabled 64 | } 65 | } 66 | 67 | // ServerCustomGrantTypeAuthentication 设置自定义授权类型认证函数 / Set custom grant type authentication functions 68 | func ServerCustomGrantTypeAuthentication(customGrantTypeAuthentication map[string]CustomGrantTypeAuthenticationFunc) ServerOption { 69 | return func(o *ServerOptions) { 70 | o.CustomGrantTypeAuthentication = customGrantTypeAuthentication 71 | } 72 | } 73 | 74 | // newServerOptions 创建server可选参数 75 | func newServerOptions(opts ...ServerOption) ServerOptions { 76 | opt := ServerOptions{ 77 | Log: &DefaultLogger{}, 78 | Issuer: DefaultJwtIssuer, 79 | DeviceAuthorizationEndpointEnabled: false, 80 | DeviceVerificationURI: "/device", 81 | IntrospectEndpointEnabled: false, 82 | TokenRevocationEnabled: false, 83 | CustomGrantTypeEnabled: false, 84 | CustomGrantTypeAuthentication: make(map[string]CustomGrantTypeAuthenticationFunc), 85 | } 86 | for _, o := range opts { 87 | o(&opt) 88 | } 89 | return opt 90 | } 91 | -------------------------------------------------------------------------------- /examples/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/nilorg/oauth2" 10 | ) 11 | 12 | var ( 13 | clients = map[string]string{ 14 | "oauth2_client": "password", 15 | } 16 | ) 17 | 18 | func main() { 19 | srv := oauth2.NewServer() 20 | srv.VerifyClient = func(ctx context.Context, basic *oauth2.ClientBasic) (err error) { 21 | pwd, ok := clients[basic.ID] 22 | if !ok { 23 | err = oauth2.ErrInvalidClient 24 | return 25 | } 26 | if basic.Secret != pwd { 27 | err = oauth2.ErrInvalidClient 28 | return 29 | } 30 | return 31 | } 32 | srv.VerifyClientID = func(ctx context.Context, clientID string) (err error) { 33 | _, ok := clients[clientID] 34 | if !ok { 35 | err = oauth2.ErrInvalidClient 36 | } 37 | return 38 | } 39 | srv.VerifyCode = func(ctx context.Context, code, clientID, redirectURI string) (value *oauth2.CodeValue, err error) { 40 | //err = oauth2.ErrUnauthorizedClient 41 | // 查询缓存/数据库中的code信息 42 | value = &oauth2.CodeValue{ 43 | ClientID: clientID, 44 | RedirectURI: redirectURI, 45 | Scope: []string{"a", "b", "c"}, 46 | } 47 | return 48 | } 49 | srv.GenerateCode = func(ctx context.Context, clientID, openID, redirectURI string, scope []string) (code string, err error) { 50 | code = oauth2.RandomCode() 51 | return 52 | } 53 | srv.VerifyRedirectURI = func(ctx context.Context, clientID, redirectURI string) (err error) { 54 | fmt.Println(clientID) 55 | fmt.Println(redirectURI) 56 | // err = oauth2.ErrInvalidRedirectURI 57 | return 58 | } 59 | 60 | srv.VerifyPassword = func(ctx context.Context, clientID, username, password string) (openID string, err error) { 61 | if username != "a" || password != "b" { 62 | err = oauth2.ErrUnauthorizedClient 63 | return 64 | } 65 | openID = "xxxx" 66 | return 67 | } 68 | 69 | srv.VerifyScope = func(ctx context.Context, scopes []string, clientID string) (err error) { 70 | // err = oauth2.ErrInvalidScope 71 | return 72 | } 73 | 74 | srv.VerifyGrantType = func(ctx context.Context, clientID, grantType string) (err error) { 75 | // err = oauth2.ErrUnauthorizedClient 76 | return 77 | } 78 | 79 | srv.AccessToken = oauth2.NewDefaultAccessToken([]byte("xxxxx")) 80 | 81 | srv.GenerateDeviceAuthorization = func(ctx context.Context, issuer, verificationURI, clientID string, scope []string) (resp *oauth2.DeviceAuthorizationResponse, err error) { 82 | resp = &oauth2.DeviceAuthorizationResponse{ 83 | DeviceCode: oauth2.RandomCode(), 84 | UserCode: oauth2.RandomUserCode(), 85 | VerificationURI: issuer + verificationURI, 86 | VerificationURIComplete: "", 87 | ExpiresIn: 0, 88 | Interval: 5, 89 | } 90 | return 91 | } 92 | 93 | srv.VerifyDeviceCode = func(ctx context.Context, deviceCode, clientID string) (value *oauth2.DeviceCodeValue, err error) { 94 | // err = oauth2.ErrAuthorizationPending 95 | return 96 | } 97 | 98 | if err := srv.InitWithError(); err != nil { 99 | panic(err) 100 | } 101 | 102 | // =============Http Default============= 103 | // http.HandleFunc("/authorize", srv.HandleAuthorize) 104 | // http.HandleFunc("/token", srv.HandleToken) 105 | // if err := http.ListenAndServe(":8003", srv); err != nil { 106 | // fmt.Printf("%+v\n", err) 107 | // } 108 | 109 | // =============Gin============= 110 | r := gin.Default() 111 | oauth2Group := r.Group("/oauth2") 112 | { 113 | oauth2Group.GET("/authorize", func(c *gin.Context) { 114 | srv.HandleAuthorize(c.Writer, c.Request) 115 | }) 116 | oauth2Group.POST("/token", func(c *gin.Context) { 117 | srv.HandleToken(c.Writer, c.Request) 118 | }) 119 | oauth2Group.POST("/device_authorization", func(c *gin.Context) { 120 | srv.HandleDeviceAuthorization(c.Writer, c.Request) 121 | }) 122 | } 123 | 124 | if err := http.ListenAndServe(":8003", r); err != nil { 125 | fmt.Printf("%+v\n", err) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import "encoding/json" 4 | 5 | // TokenResponse 令牌响应结构 / Token response structure 6 | type TokenResponse struct { 7 | AccessToken string `json:"access_token"` // 访问令牌 / Access token 8 | TokenType string `json:"token_type,omitempty"` // 令牌类型 / Token type (e.g., Bearer) 9 | ExpiresIn int64 `json:"expires_in"` // 过期时间(秒) / Expiration time in seconds 10 | RefreshToken string `json:"refresh_token,omitempty"` // 刷新令牌 / Refresh token 11 | Data interface{} `json:"data,omitempty"` // 自定义数据 / Custom data 12 | Scope string `json:"scope,omitempty"` // 授权范围 / Authorized scope 13 | IDToken string `json:"id_token,omitempty"` // ID令牌 / ID token (OpenID Connect) 14 | } 15 | 16 | // DeviceAuthorizationResponse 设备授权响应结构 / Device authorization response (RFC 8628) 17 | type DeviceAuthorizationResponse struct { 18 | DeviceCode string `json:"device_code"` // 设备码 / Device verification code 19 | UserCode string `json:"user_code"` // 用户码 / User verification code 20 | VerificationURI string `json:"verification_uri"` // 验证URI / Verification URI 21 | VerificationURIComplete string `json:"verification_uri_complete,omitempty"` // 完整验证URI / Complete verification URI with user code 22 | ExpiresIn int64 `json:"expires_in"` // 过期时间(秒) / Expiration time in seconds 23 | Interval int `json:"interval"` // 轮询间隔(秒) / Polling interval in seconds 24 | } 25 | 26 | // IntrospectionResponse 令牌内省响应结构 / Token introspection response (RFC 7662) 27 | type IntrospectionResponse struct { 28 | Active bool `json:"active"` // 令牌是否有效 / Whether the token is active 29 | ClientID string `json:"client_id,omitempty"` // 客户端ID / Client identifier 30 | Username string `json:"username,omitempty"` // 用户名 / Resource owner username 31 | Scope string `json:"scope,omitempty"` // 授权范围 / Token scope 32 | Sub string `json:"sub,omitempty"` // 主体 / Subject (user identifier) 33 | Aud string `json:"aud,omitempty"` // 受众 / Audience 34 | Iss int64 `json:"iss,omitempty"` // 颁发者 / Issuer 35 | Exp int64 `json:"exp,omitempty"` // 过期时间 / Expiration time 36 | } 37 | 38 | // ErrorResponse 错误响应结构 / Error response structure 39 | type ErrorResponse struct { 40 | Error string `json:"error"` // 错误码 / Error code 41 | } 42 | 43 | // CodeValue 授权码存储值 / Authorization code storage value 44 | type CodeValue struct { 45 | ClientID string `json:"client_id"` // 客户端ID / Client identifier 46 | OpenID string `json:"open_id"` // 用户唯一标识 / User unique identifier 47 | RedirectURI string `json:"redirect_uri"` // 重定向URI / Redirect URI 48 | Scope []string `json:"scope"` // 授权范围 / Authorized scopes 49 | } 50 | 51 | // MarshalBinary 序列化为JSON二进制 / Serialize to JSON binary 52 | func (code *CodeValue) MarshalBinary() ([]byte, error) { 53 | return json.Marshal(code) 54 | } 55 | 56 | // UnmarshalBinary 从JSON二进制反序列化 / Deserialize from JSON binary 57 | func (code *CodeValue) UnmarshalBinary(data []byte) error { 58 | return json.Unmarshal(data, code) 59 | } 60 | 61 | // DeviceCodeValue 设备码存储值 / Device code storage value 62 | type DeviceCodeValue struct { 63 | OpenID string `json:"open_id"` // 用户唯一标识 / User unique identifier 64 | Scope []string `json:"scope"` // 授权范围 / Authorized scopes 65 | } 66 | 67 | // MarshalBinary 序列化为JSON二进制 / Serialize to JSON binary 68 | func (code *DeviceCodeValue) MarshalBinary() ([]byte, error) { 69 | return json.Marshal(code) 70 | } 71 | 72 | // UnmarshalBinary 从JSON二进制反序列化 / Deserialize from JSON binary 73 | func (code *DeviceCodeValue) UnmarshalBinary(data []byte) error { 74 | return json.Unmarshal(data, code) 75 | } 76 | 77 | // ClientBasic 客户端基础信息 / Client basic credentials 78 | type ClientBasic struct { 79 | ID string `json:"client_id"` // 客户端ID / Client identifier 80 | Secret string `json:"client_secret"` // 客户端密钥 / Client secret 81 | } 82 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | ) 7 | 8 | var ( 9 | // ErrInvalidRequest 无效的请求 10 | ErrInvalidRequest = errors.New("invalid_request") 11 | // ErrUnauthorizedClient 未经授权的客户端 12 | ErrUnauthorizedClient = errors.New("unauthorized_client") 13 | // ErrAccessDenied 拒绝访问 14 | ErrAccessDenied = errors.New("access_denied") 15 | // ErrUnsupportedResponseType 不支持的response类型 16 | ErrUnsupportedResponseType = errors.New("unsupported_response_type") 17 | // ErrUnsupportedGrantType 不支持的grant类型 18 | ErrUnsupportedGrantType = errors.New("unsupported_grant_type") 19 | // ErrInvalidGrant 无效的grant 20 | ErrInvalidGrant = errors.New("invalid_grant") 21 | // ErrInvalidScope 无效scope 22 | ErrInvalidScope = errors.New("invalid_scope") 23 | // ErrTemporarilyUnavailable 暂时不可用 24 | ErrTemporarilyUnavailable = errors.New("temporarily_unavailable") 25 | // ErrServerError 服务器错误 26 | ErrServerError = errors.New("server_error") 27 | // ErrInvalidClient 无效的客户 28 | ErrInvalidClient = errors.New("invalid_client") 29 | // ErrExpiredToken 过期的令牌 30 | ErrExpiredToken = errors.New("expired_token") 31 | // ErrAuthorizationPending 授权待定 32 | // https://tools.ietf.org/html/rfc8628#section-3.5 33 | ErrAuthorizationPending = errors.New("authorization_pending") 34 | // ErrSlowDown 轮询太频繁 35 | // https://tools.ietf.org/html/rfc8628#section-3.5 36 | ErrSlowDown = errors.New("slow_down") 37 | // ErrUnsupportedTokenType 不支持的令牌类型 38 | // https://tools.ietf.org/html/rfc7009#section-4.1.1 39 | ErrUnsupportedTokenType = errors.New("unsupported_token_type") 40 | ) 41 | 42 | var ( 43 | // ErrVerifyClientFuncNil VerifyClient函数未设置 / VerifyClient function is not set 44 | ErrVerifyClientFuncNil = errors.New("OAuth2 Server VerifyClient Is Nil") 45 | // ErrVerifyClientIDFuncNil VerifyClientID函数未设置 / VerifyClientID function is not set 46 | ErrVerifyClientIDFuncNil = errors.New("OAuth2 Server VerifyClientID Is Nil") 47 | // ErrVerifyPasswordFuncNil VerifyPassword函数未设置 / VerifyPassword function is not set 48 | ErrVerifyPasswordFuncNil = errors.New("OAuth2 Server VerifyPassword Is Nil") 49 | // ErrVerifyRedirectURIFuncNil VerifyRedirectURI函数未设置 / VerifyRedirectURI function is not set 50 | ErrVerifyRedirectURIFuncNil = errors.New("OAuth2 Server VerifyRedirectURI Is Nil") 51 | // ErrGenerateCodeFuncNil GenerateCode函数未设置 / GenerateCode function is not set 52 | ErrGenerateCodeFuncNil = errors.New("OAuth2 Server GenerateCode Is Nil") 53 | // ErrVerifyCodeFuncNil VerifyCode函数未设置 / VerifyCode function is not set 54 | ErrVerifyCodeFuncNil = errors.New("OAuth2 Server VerifyCode Is Nil") 55 | // ErrVerifyScopeFuncNil VerifyScope函数未设置 / VerifyScope function is not set 56 | ErrVerifyScopeFuncNil = errors.New("OAuth2 Server VerifyScope Is Nil") 57 | // ErrGenerateAccessTokenFuncNil GenerateAccessToken函数未设置 / GenerateAccessToken function is not set 58 | ErrGenerateAccessTokenFuncNil = errors.New("OAuth2 Server GenerateAccessTokenFunc Is Nil") 59 | // ErrGenerateDeviceAuthorizationFuncNil GenerateDeviceAuthorization函数未设置 / GenerateDeviceAuthorization function is not set 60 | ErrGenerateDeviceAuthorizationFuncNil = errors.New("OAuth2 Server GenerateDeviceAuthorizationFunc Is Nil") 61 | // ErrVerifyDeviceCodeFuncNil VerifyDeviceCode函数未设置 / VerifyDeviceCode function is not set 62 | ErrVerifyDeviceCodeFuncNil = errors.New("OAuth2 Server ErrVerifyDeviceCodeFunc Is Nil") 63 | // ErrRefreshAccessTokenFuncNil RefreshAccessToken函数未设置 / RefreshAccessToken function is not set 64 | ErrRefreshAccessTokenFuncNil = errors.New("OAuth2 Server ErrRefreshAccessTokenFuncNil Is Nil") 65 | // ErrParseAccessTokenFuncNil ParseAccessToken函数未设置 / ParseAccessToken function is not set 66 | ErrParseAccessTokenFuncNil = errors.New("OAuth2 Server ParseAccessTokenFunc Is Nil") 67 | // ErrVerifyIntrospectionTokenFuncNil VerifyIntrospectionToken函数未设置 / VerifyIntrospectionToken function is not set 68 | ErrVerifyIntrospectionTokenFuncNil = errors.New("OAuth2 Server VerifyIntrospectionToken Is Nil") 69 | // ErrTokenRevocationFuncNil TokenRevocation函数未设置 / TokenRevocation function is not set 70 | ErrTokenRevocationFuncNil = errors.New("OAuth2 Server TokenRevocation Is Nil") 71 | // ErrVerifyGrantTypeFuncNil VerifyGrantType函数未设置 / VerifyGrantType function is not set 72 | ErrVerifyGrantTypeFuncNil = errors.New("OAuth2 Server VerifyGrantType Is Nil") 73 | // ErrInvalidAccessToken 无效的访问令牌 74 | ErrInvalidAccessToken = errors.New("invalid_access_token") 75 | // ErrInvalidRedirectURI 无效的RedirectURI 76 | ErrInvalidRedirectURI = errors.New("invalid_redirect_uri") 77 | // ErrStateValueDidNotMatch state值不匹配 / State value did not match 78 | ErrStateValueDidNotMatch = errors.New("state value did not match") 79 | // ErrMissingAccessToken 缺少访问令牌 / Missing access token in request 80 | ErrMissingAccessToken = errors.New("missing access token") 81 | // ErrAccessToken AccessToken接口未设置 / AccessToken interface is not set 82 | ErrAccessToken = errors.New("OAuth2 Server AccessToken Is Nil") 83 | ) 84 | 85 | var ( 86 | // Errors 错误映射表,用于从错误字符串查找错误对象 / Error map for looking up error objects from error strings 87 | Errors = map[string]error{ 88 | ErrVerifyClientFuncNil.Error(): ErrVerifyClientFuncNil, 89 | ErrInvalidAccessToken.Error(): ErrInvalidAccessToken, 90 | ErrStateValueDidNotMatch.Error(): ErrStateValueDidNotMatch, 91 | ErrMissingAccessToken.Error(): ErrMissingAccessToken, 92 | 93 | ErrInvalidRequest.Error(): ErrInvalidRequest, 94 | ErrUnauthorizedClient.Error(): ErrUnauthorizedClient, 95 | ErrAccessDenied.Error(): ErrAccessDenied, 96 | ErrUnsupportedResponseType.Error(): ErrUnsupportedResponseType, 97 | ErrUnsupportedGrantType.Error(): ErrUnsupportedGrantType, 98 | ErrInvalidGrant.Error(): ErrInvalidGrant, 99 | ErrInvalidScope.Error(): ErrInvalidScope, 100 | ErrTemporarilyUnavailable.Error(): ErrTemporarilyUnavailable, 101 | ErrServerError.Error(): ErrServerError, 102 | ErrInvalidClient.Error(): ErrInvalidClient, 103 | ErrExpiredToken.Error(): ErrExpiredToken, 104 | ErrAuthorizationPending.Error(): ErrAuthorizationPending, 105 | ErrSlowDown.Error(): ErrSlowDown, 106 | ErrUnsupportedTokenType.Error(): ErrUnsupportedTokenType, 107 | } 108 | // ErrStatusCodes 错误对应的HTTP状态码映射表 / HTTP status codes mapping for errors 109 | ErrStatusCodes = map[error]int{ 110 | ErrInvalidRequest: http.StatusBadRequest, // 400 111 | ErrUnauthorizedClient: http.StatusUnauthorized, // 401 112 | ErrAccessDenied: http.StatusForbidden, // 403 113 | ErrUnsupportedResponseType: http.StatusUnauthorized, // 401 114 | ErrInvalidScope: http.StatusBadRequest, // 400 115 | ErrServerError: http.StatusInternalServerError, // 400 116 | ErrTemporarilyUnavailable: http.StatusServiceUnavailable, // 503 117 | ErrInvalidClient: http.StatusUnauthorized, // 401 118 | ErrInvalidGrant: http.StatusUnauthorized, // 401 119 | ErrUnsupportedGrantType: http.StatusUnauthorized, // 401 120 | ErrExpiredToken: http.StatusUnauthorized, // 401 121 | ErrAuthorizationPending: http.StatusPreconditionRequired, // 428 122 | ErrSlowDown: http.StatusForbidden, // 403 https://tools.ietf.org/html/rfc6749#section-5.2 123 | ErrUnsupportedTokenType: http.StatusServiceUnavailable, // 503 https://tools.ietf.org/html/rfc7009#section-2.2.1 124 | } 125 | ) 126 | -------------------------------------------------------------------------------- /func.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "github.com/nilorg/pkg/slice" 10 | sdkStrings "github.com/nilorg/sdk/strings" 11 | ) 12 | 13 | // VerifyClientFunc 验证客户端委托 14 | type VerifyClientFunc func(ctx context.Context, basic *ClientBasic) (err error) 15 | 16 | // VerifyClientIDFunc 验证客户端ID委托 17 | type VerifyClientIDFunc func(ctx context.Context, clientID string) (err error) 18 | 19 | // VerifyRedirectURIFunc 验证RedirectURI委托 20 | type VerifyRedirectURIFunc func(ctx context.Context, clientID, redirectURI string) (err error) 21 | 22 | // GenerateCodeFunc 生成Code委托 23 | type GenerateCodeFunc func(ctx context.Context, clientID, openID, redirectURI string, scope []string) (code string, err error) 24 | 25 | // VerifyCodeFunc 验证Code委托 26 | type VerifyCodeFunc func(ctx context.Context, code, clientID, redirectURI string) (value *CodeValue, err error) 27 | 28 | // VerifyPasswordFunc 验证账号密码委托 29 | type VerifyPasswordFunc func(ctx context.Context, clientID, username, password string) (openID string, err error) 30 | 31 | // VerifyScopeFunc 验证范围委托 32 | type VerifyScopeFunc func(ctx context.Context, scope []string, clientID string) (err error) 33 | 34 | // GenerateDeviceAuthorizationFunc 生成设备授权 35 | type GenerateDeviceAuthorizationFunc func(ctx context.Context, issuer, verificationURI, clientID string, scope []string) (resp *DeviceAuthorizationResponse, err error) 36 | 37 | // VerifyDeviceCodeFunc 验证DeviceCode委托 38 | type VerifyDeviceCodeFunc func(ctx context.Context, deviceCode, clientID string) (value *DeviceCodeValue, err error) 39 | 40 | // VerifyIntrospectionTokenFunc 验证IntrospectionToken委托 41 | type VerifyIntrospectionTokenFunc func(ctx context.Context, token, clientID string, tokenTypeHint ...string) (resp *IntrospectionResponse, err error) 42 | 43 | // TokenRevocationFunc Token撤销委托 44 | // https://tools.ietf.org/html/rfc7009#section-2.2 45 | type TokenRevocationFunc func(ctx context.Context, token, clientID string, tokenTypeHint ...string) 46 | 47 | // CustomGrantTypeAuthenticationFunc 自定义GrantType身份验证委托 48 | type CustomGrantTypeAuthenticationFunc func(ctx context.Context, client *ClientBasic, req *http.Request) (openID string, err error) 49 | 50 | // VerifyGrantTypeFunc 验证授权类型委托 51 | type VerifyGrantTypeFunc func(ctx context.Context, clientID, grantType string) (err error) 52 | 53 | // GenerateAccessTokenFunc 生成AccessToken委托 54 | type GenerateAccessTokenFunc func(ctx context.Context, issuer, clientID, scope, openID string, code *CodeValue) (token *TokenResponse, err error) 55 | 56 | // NewDefaultGenerateAccessToken 创建默认生成AccessToken方法 57 | func NewDefaultGenerateAccessToken(jwtVerifyKey []byte) GenerateAccessTokenFunc { 58 | return func(ctx context.Context, issuer, clientID, scope, openID string, codeVlue *CodeValue) (token *TokenResponse, err error) { 59 | scopeSplit := sdkStrings.Split(scope, " ") 60 | accessJwtClaims := NewJwtClaims(issuer, clientID, scope, openID) 61 | if codeVlue != nil { 62 | if len(scopeSplit) > 0 && !slice.IsEqual(scopeSplit, codeVlue.Scope) { 63 | accessJwtClaims = NewJwtClaims(issuer, clientID, strings.Join(codeVlue.Scope, " "), openID) 64 | } 65 | } 66 | var tokenStr string 67 | tokenStr, err = NewHS256JwtClaimsToken(accessJwtClaims, jwtVerifyKey) 68 | if err != nil { 69 | err = ErrServerError 70 | return 71 | } 72 | 73 | refreshAccessJwtClaims := NewJwtClaims(issuer, clientID, ScopeRefreshToken, "") 74 | refreshAccessJwtClaims.ID = tokenStr 75 | var refreshTokenStr string 76 | refreshTokenStr, err = NewHS256JwtClaimsToken(refreshAccessJwtClaims, jwtVerifyKey) 77 | if err != nil { 78 | err = ErrServerError 79 | return 80 | } 81 | token = &TokenResponse{ 82 | AccessToken: tokenStr, 83 | TokenType: TokenTypeBearer, 84 | ExpiresIn: accessJwtClaims.ExpiresAt, 85 | RefreshToken: refreshTokenStr, 86 | Scope: scope, 87 | } 88 | return 89 | } 90 | } 91 | 92 | // ParseAccessTokenFunc 解析AccessToken为JwtClaims委托 93 | type ParseAccessTokenFunc func(ctx context.Context, accessToken string) (claims *JwtClaims, err error) 94 | 95 | // NewDefaultRefreshAccessToken 创建默认刷新AccessToken方法 96 | func NewDefaultRefreshAccessToken(jwtVerifyKey []byte) RefreshAccessTokenFunc { 97 | return func(ctx context.Context, clientID, refreshToken string) (token *TokenResponse, err error) { 98 | var refreshTokenClaims *JwtClaims 99 | refreshTokenClaims, err = ParseHS256JwtClaimsToken(refreshToken, jwtVerifyKey) 100 | if err != nil { 101 | return 102 | } 103 | if refreshTokenClaims.Subject != clientID { 104 | err = ErrUnauthorizedClient 105 | return 106 | } 107 | if refreshTokenClaims.Scope != ScopeRefreshToken { 108 | err = ErrInvalidScope 109 | return 110 | } 111 | refreshTokenClaims.ExpiresAt = time.Now().Add(AccessTokenExpire).Unix() 112 | 113 | var tokenClaims *JwtClaims 114 | tokenClaims, err = ParseHS256JwtClaimsToken(refreshTokenClaims.ID, jwtVerifyKey) 115 | if err != nil { 116 | return 117 | } 118 | if tokenClaims.Subject != clientID { 119 | err = ErrUnauthorizedClient 120 | return 121 | } 122 | tokenClaims.ExpiresAt = time.Now().Add(AccessTokenExpire).Unix() 123 | 124 | var refreshTokenStr string 125 | refreshTokenStr, err = NewHS256JwtClaimsToken(refreshTokenClaims, jwtVerifyKey) 126 | if err != nil { 127 | return 128 | } 129 | var tokenStr string 130 | tokenStr, err = NewHS256JwtClaimsToken(tokenClaims, jwtVerifyKey) 131 | token = &TokenResponse{ 132 | AccessToken: tokenStr, 133 | RefreshToken: refreshTokenStr, 134 | TokenType: TokenTypeBearer, 135 | ExpiresIn: refreshTokenClaims.ExpiresAt, 136 | Scope: tokenClaims.Scope, 137 | } 138 | return 139 | } 140 | } 141 | 142 | // RefreshAccessTokenFunc 刷新AccessToken委托 143 | type RefreshAccessTokenFunc func(ctx context.Context, clientID, refreshToken string) (token *TokenResponse, err error) 144 | 145 | // NewDefaultParseAccessToken 创建默认解析AccessToken方法 146 | func NewDefaultParseAccessToken(jwtVerifyKey []byte) ParseAccessTokenFunc { 147 | return func(ctx context.Context, accessToken string) (claims *JwtClaims, err error) { 148 | return ParseHS256JwtClaimsToken(accessToken, jwtVerifyKey) 149 | } 150 | } 151 | 152 | // AccessTokener AccessToken接口 153 | type AccessTokener interface { 154 | Generate(ctx context.Context, issuer, clientID, scope, openID string, code *CodeValue) (token *TokenResponse, err error) 155 | Refresh(ctx context.Context, clientID, refreshToken string) (token *TokenResponse, err error) 156 | Parse(ctx context.Context, accessToken string) (claims *JwtClaims, err error) 157 | } 158 | 159 | type DefaultAccessToken struct { 160 | AccessTokener 161 | JwtVerifyKey []byte 162 | } 163 | 164 | func NewDefaultAccessToken(jwtVerifyKey []byte) *DefaultAccessToken { 165 | return &DefaultAccessToken{ 166 | JwtVerifyKey: jwtVerifyKey, 167 | } 168 | } 169 | 170 | // Generate 生成AccessToken 171 | func (d *DefaultAccessToken) Generate(ctx context.Context, issuer, clientID, scope, openID string, code *CodeValue) (token *TokenResponse, err error) { 172 | return NewDefaultGenerateAccessToken(d.JwtVerifyKey)(ctx, issuer, clientID, scope, openID, code) 173 | } 174 | 175 | // Refresh 刷新AccessToken 176 | func (d *DefaultAccessToken) Refresh(ctx context.Context, clientID, refreshToken string) (token *TokenResponse, err error) { 177 | return NewDefaultRefreshAccessToken(d.JwtVerifyKey)(ctx, clientID, refreshToken) 178 | } 179 | 180 | // Parse 解析AccessToken 181 | func (d *DefaultAccessToken) Parse(ctx context.Context, accessToken string) (claims *JwtClaims, err error) { 182 | return NewDefaultParseAccessToken(d.JwtVerifyKey)(ctx, accessToken) 183 | } 184 | -------------------------------------------------------------------------------- /jwt.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "crypto/subtle" 5 | "fmt" 6 | "time" 7 | 8 | jose "github.com/go-jose/go-jose/v4" 9 | "github.com/go-jose/go-jose/v4/jwt" 10 | "github.com/nilorg/pkg/slice" 11 | "github.com/nilorg/sdk/strings" 12 | ) 13 | 14 | // 参考 https://github.com/dgrijalva/jwt-go/blob/master/claims.go 15 | 16 | // ----- helpers 17 | 18 | func verifyAud(aud []string, cmp []string, required bool) bool { 19 | if len(aud) == 0 { 20 | return !required 21 | } 22 | return slice.IsSubset(cmp, aud) 23 | } 24 | 25 | func verifyExp(exp int64, now int64, required bool) bool { 26 | if exp == 0 { 27 | return !required 28 | } 29 | return now <= exp 30 | } 31 | 32 | func verifyIat(iat int64, now int64, required bool) bool { 33 | if iat == 0 { 34 | return !required 35 | } 36 | return now >= iat 37 | } 38 | 39 | func verifyIss(iss string, cmp string, required bool) bool { 40 | if iss == "" { 41 | return !required 42 | } 43 | return subtle.ConstantTimeCompare([]byte(iss), []byte(cmp)) != 0 44 | } 45 | 46 | func verifyNbf(nbf int64, now int64, required bool) bool { 47 | if nbf == 0 { 48 | return !required 49 | } 50 | return now >= nbf 51 | } 52 | 53 | func verifyScope(scope []string, cmp []string, required bool) bool { 54 | if len(scope) == 0 { 55 | return !required 56 | } 57 | return slice.IsSubset(cmp, scope) 58 | } 59 | 60 | // JwtStandardClaims as referenced at 61 | // https://tools.ietf.org/html/rfc7519#section-4.1 62 | type JwtStandardClaims struct { 63 | Audience []string `json:"aud,omitempty"` 64 | ExpiresAt int64 `json:"exp,omitempty"` 65 | ID string `json:"jti,omitempty"` 66 | IssuedAt int64 `json:"iat,omitempty"` 67 | Issuer string `json:"iss,omitempty"` 68 | NotBefore int64 `json:"nbf,omitempty"` 69 | Subject string `json:"sub,omitempty"` 70 | } 71 | 72 | // Valid time based claims "exp, iat, nbf". 73 | // There is no accounting for clock skew. 74 | // As well, if any of the above claims are not in the token, it will still 75 | // be considered a valid claim. 76 | func (c JwtStandardClaims) Valid() error { 77 | now := time.Now().Unix() 78 | // The claims below are optional, by default, so if they are set to the 79 | // default value in Go, let's not fail the verification for them. 80 | if !c.VerifyExpiresAt(now, false) { 81 | delta := time.Unix(now, 0).Sub(time.Unix(c.ExpiresAt, 0)) 82 | return fmt.Errorf("token is expired by %v", delta) 83 | } 84 | if !c.VerifyIssuedAt(now, false) { 85 | return fmt.Errorf("Token used before issued") 86 | } 87 | if !c.VerifyNotBefore(now, false) { 88 | return fmt.Errorf("token is not valid yet") 89 | } 90 | return nil 91 | } 92 | 93 | // VerifyAudience Compares the aud claim against cmp. 94 | // If required is false, this method will return true if the value matches or is unset 95 | // 如果required为false,如果值匹配或未设置,此方法将返回true 96 | func (c *JwtStandardClaims) VerifyAudience(cmp []string, req bool) bool { 97 | return verifyAud(c.Audience, cmp, req) 98 | } 99 | 100 | // VerifyExpiresAt Compares the exp claim against cmp. 101 | // If required is false, this method will return true if the value matches or is unset 102 | // 如果required为false,如果值匹配或未设置,此方法将返回true 103 | func (c *JwtStandardClaims) VerifyExpiresAt(cmp int64, req bool) bool { 104 | return verifyExp(c.ExpiresAt, cmp, req) 105 | } 106 | 107 | // VerifyIssuedAt Compares the iat claim against cmp. 108 | // If required is false, this method will return true if the value matches or is unset 109 | // 如果required为false,如果值匹配或未设置,此方法将返回true 110 | func (c *JwtStandardClaims) VerifyIssuedAt(cmp int64, req bool) bool { 111 | return verifyIat(c.IssuedAt, cmp, req) 112 | } 113 | 114 | // VerifyIssuer Compares the iss claim against cmp. 115 | // If required is false, this method will return true if the value matches or is unset 116 | // 如果required为false,如果值匹配或未设置,此方法将返回true 117 | func (c *JwtStandardClaims) VerifyIssuer(cmp string, req bool) bool { 118 | return verifyIss(c.Issuer, cmp, req) 119 | } 120 | 121 | // VerifyNotBefore Compares the nbf claim against cmp. 122 | // If required is false, this method will return true if the value matches or is unset 123 | // 如果required为false,如果值匹配或未设置,此方法将返回true 124 | func (c *JwtStandardClaims) VerifyNotBefore(cmp int64, req bool) bool { 125 | return verifyNbf(c.NotBefore, cmp, req) 126 | } 127 | 128 | // JwtClaims 在jwt标准上的扩展 129 | type JwtClaims struct { 130 | JwtStandardClaims 131 | Scope string `json:"scope,omitempty"` 132 | } 133 | 134 | // VerifyScope Compares the aud claim against cmp. 135 | // If required is false, this method will return true if the value matches or is unset 136 | // 如果required为false,如果值匹配或未设置,此方法将返回true 137 | func (c *JwtClaims) VerifyScope(scope string, req bool) bool { 138 | source := strings.Split(c.Scope, " ") 139 | array := strings.Split(scope, " ") 140 | return verifyScope(source, array, req) 141 | } 142 | 143 | // NewJwtClaims ... 144 | func NewJwtClaims(issuer, audience, scope, openID string) *JwtClaims { 145 | currTime := time.Now() 146 | return &JwtClaims{ 147 | JwtStandardClaims: JwtStandardClaims{ 148 | // Issuer = iss,令牌颁发者。它表示该令牌是由谁创建的 149 | Issuer: issuer, 150 | // Subject = sub,令牌的主体。它表示该令牌是关于谁的 151 | Subject: openID, 152 | // Audience = aud,令牌的受众。它表示令牌的接收者 153 | Audience: []string{audience}, 154 | // ExpiresAt = exp,令牌的过期时间戳。它表示令牌将在何时过期 155 | ExpiresAt: currTime.Add(AccessTokenExpire).Unix(), 156 | // NotBefore = nbf,令牌的生效时的时间戳。它表示令牌从什么时候开始生效 157 | NotBefore: currTime.Unix(), 158 | // IssuedAt = iat,令牌颁发时的时间戳。它表示令牌是何时被创建的 159 | IssuedAt: currTime.Unix(), 160 | }, 161 | Scope: scope, 162 | } 163 | } 164 | 165 | func newJwtToken(v interface{}, algorithm string, key interface{}) (token string, err error) { 166 | var sig jose.Signer 167 | sig, err = jose.NewSigner( 168 | jose.SigningKey{ 169 | Algorithm: jose.SignatureAlgorithm(algorithm), 170 | Key: key, 171 | }, 172 | (&jose.SignerOptions{}).WithType("JWT"), 173 | ) 174 | if err != nil { 175 | return 176 | } 177 | token, err = jwt.Signed(sig).Claims(v).Serialize() 178 | return 179 | } 180 | 181 | // NewJwtToken ... 182 | func NewJwtToken(v interface{}, algorithm string, key interface{}) (string, error) { 183 | return newJwtToken(v, algorithm, key) 184 | } 185 | 186 | // NewJwtClaimsToken ... 187 | func NewJwtClaimsToken(claims *JwtClaims, algorithm string, key interface{}) (string, error) { 188 | return newJwtToken(claims, algorithm, key) 189 | } 190 | 191 | // NewJwtStandardClaimsToken ... 192 | func NewJwtStandardClaimsToken(claims *JwtStandardClaims, algorithm string, key interface{}) (string, error) { 193 | return newJwtToken(claims, algorithm, key) 194 | } 195 | 196 | // NewHS256JwtClaimsToken ... 197 | func NewHS256JwtClaimsToken(claims *JwtClaims, jwtVerifyKey []byte) (string, error) { 198 | return newJwtToken(claims, "HS256", jwtVerifyKey) 199 | } 200 | 201 | // parseJwtToken ... 202 | func parseJwtToken(token string, algorithm string, key interface{}, dest ...interface{}) (err error) { 203 | var ( 204 | tok *jwt.JSONWebToken 205 | ) 206 | tok, err = jwt.ParseSigned(token, []jose.SignatureAlgorithm{ 207 | jose.SignatureAlgorithm(algorithm), 208 | }) 209 | if err != nil { 210 | return 211 | } 212 | err = tok.Claims(key, dest...) 213 | return 214 | } 215 | 216 | // ParseJwtClaimsToken ... 217 | func ParseJwtClaimsToken(token string, algorithm string, key interface{}) (claims *JwtClaims, err error) { 218 | claims = new(JwtClaims) 219 | err = parseJwtToken(token, algorithm, key, claims) 220 | return 221 | } 222 | 223 | // ParseJwtStandardClaimsToken ... 224 | func ParseJwtStandardClaimsToken(token string, algorithm string, key interface{}) (claims *JwtStandardClaims, err error) { 225 | claims = new(JwtStandardClaims) 226 | err = parseJwtToken(token, algorithm, key, claims) 227 | return 228 | } 229 | 230 | // ParseHS256JwtClaimsToken ... 231 | func ParseHS256JwtClaimsToken(token string, jwtVerifyKey []byte) (claims *JwtClaims, err error) { 232 | return ParseJwtClaimsToken(token, "HS256", jwtVerifyKey) 233 | } 234 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # oauth2 2 | 3 | # Usage 4 | ```bash 5 | go get -u github.com/nilorg/oauth2 6 | ``` 7 | # Import 8 | ```bash 9 | import "github.com/nilorg/oauth2" 10 | ``` 11 | 12 | # 例子 13 | 14 | [oauth2-server](https://github.com/nilorg/oauth2-server) 15 | 16 | [server/client](https://github.com/nilorg/oauth2/tree/master/examples) 17 | 18 | # 文档参考 19 | 1. [《理解OAuth 2.0》阮一峰](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) 20 | 2. [《RFC 6749》](https://tools.ietf.org/html/rfc6749) | [《RFC 6749》](http://www.rfcreader.com/#rfc6749) 21 | 3. [《OAuth 2.0 Device Authorization Grant(RFC8628)》](https://tools.ietf.org/html/rfc8628) 22 | 4. [《OAuth 2.0 Token Introspection(RFC7662)》](https://tools.ietf.org/html/rfc7662) 23 | 5. [《OAuth 2.0 Token Revocation(RFC7009)》](https://tools.ietf.org/html/rfc7009) 24 | 25 | 26 | ### AuthorizationCode 27 | 授权码模式(authorization code)是功能最完整、流程最严密的授权模式。 28 | 29 | 它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。 30 | ### Implicit 31 | 简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。 32 | 33 | 所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。 34 | ### ResourceOwnerPasswordCredentials 35 | 密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。 36 | 37 | 客户端使用这些信息,向"服务商提供商"索要授权。 38 | 39 | 在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。 40 | 41 | 这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。 42 | 43 | 而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。 44 | ### ClientCredentials 45 | 客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。 46 | 47 | 严格地说,客户端模式并不属于OAuth框架所要解决的问题。 48 | 49 | 在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。 50 | 51 | ### DeviceCode 52 | 设备模式(Device Code) 53 | 54 | ### TokenIntrospection 55 | 内省端点(Token Introspection) 56 | ### TokenRevocation 57 | Token销毁端点(Token Revocation) 58 | 59 | # Server 60 | 61 | ```go 62 | package main 63 | 64 | import ( 65 | "context" 66 | "fmt" 67 | "net/http" 68 | 69 | "github.com/gin-gonic/gin" 70 | "github.com/nilorg/oauth2" 71 | ) 72 | 73 | var ( 74 | clients = map[string]string{ 75 | "oauth2_client": "password", 76 | } 77 | ) 78 | 79 | func main() { 80 | srv := oauth2.NewServer() 81 | srv.VerifyClient = func(ctx context.Context, basic *oauth2.ClientBasic) (err error) { 82 | pwd, ok := clients[basic.ID] 83 | if !ok { 84 | err = oauth2.ErrInvalidClient 85 | return 86 | } 87 | if basic.Secret != pwd { 88 | err = oauth2.ErrInvalidClient 89 | return 90 | } 91 | return 92 | } 93 | srv.VerifyClientID = func(ctx context.Context, clientID string) (err error) { 94 | _, ok := clients[clientID] 95 | if !ok { 96 | err = oauth2.ErrInvalidClient 97 | } 98 | return 99 | } 100 | srv.VerifyCode = func(ctx context.Context, code, clientID, redirectURI string) (value *oauth2.CodeValue, err error) { 101 | //err = oauth2.ErrUnauthorizedClient 102 | // 查询缓存/数据库中的code信息 103 | value = &oauth2.CodeValue{ 104 | ClientID: clientID, 105 | RedirectURI: redirectURI, 106 | Scope: []string{"a", "b", "c"}, 107 | } 108 | return 109 | } 110 | srv.GenerateCode = func(ctx context.Context, clientID, openID, redirectURI string, scope []string) (code string, err error) { 111 | code = oauth2.RandomCode() 112 | return 113 | } 114 | srv.VerifyRedirectURI = func(ctx context.Context, clientID, redirectURI string) (err error) { 115 | fmt.Println(clientID) 116 | fmt.Println(redirectURI) 117 | // err = oauth2.ErrInvalidRedirectURI 118 | return 119 | } 120 | 121 | srv.VerifyPassword = func(ctx context.Context, clientID, username, password string) (openID string, err error) { 122 | if username != "a" || password != "b" { 123 | err = oauth2.ErrUnauthorizedClient 124 | return 125 | } 126 | openID = "xxxx" 127 | return 128 | } 129 | 130 | srv.VerifyScope = func(ctx context.Context, scopes []string, clientID string) (err error) { 131 | // err = oauth2.ErrInvalidScope 132 | return 133 | } 134 | 135 | srv.VerifyGrantType = func(ctx context.Context, clientID, grantType string) (err error) { 136 | // err = oauth2.ErrUnauthorizedClient 137 | return 138 | } 139 | 140 | srv.AccessToken = oauth2.NewDefaultAccessToken([]byte("xxxxx")) 141 | 142 | srv.GenerateDeviceAuthorization = func(ctx context.Context, issuer, verificationURI, clientID string, scope []string) (resp *oauth2.DeviceAuthorizationResponse, err error) { 143 | resp = &oauth2.DeviceAuthorizationResponse{ 144 | DeviceCode: oauth2.RandomCode(), 145 | UserCode: oauth2.RandomUserCode(), 146 | VerificationURI: issuer + verificationURI, 147 | VerificationURIComplete: "", 148 | ExpiresIn: 0, 149 | Interval: 5, 150 | } 151 | return 152 | } 153 | 154 | srv.VerifyDeviceCode = func(ctx context.Context, deviceCode, clientID string) (value *oauth2.DeviceCodeValue, err error) { 155 | // err = oauth2.ErrAuthorizationPending 156 | return 157 | } 158 | 159 | if err := srv.InitWithError(); err != nil { 160 | panic(err) 161 | } 162 | 163 | // =============Http Default============= 164 | // http.HandleFunc("/authorize", srv.HandleAuthorize) 165 | // http.HandleFunc("/token", srv.HandleToken) 166 | // if err := http.ListenAndServe(":8003", srv); err != nil { 167 | // fmt.Printf("%+v\n", err) 168 | // } 169 | 170 | // =============Gin============= 171 | r := gin.Default() 172 | oauth2Group := r.Group("/oauth2") 173 | { 174 | oauth2Group.GET("/authorize", func(c *gin.Context) { 175 | srv.HandleAuthorize(c.Writer, c.Request) 176 | }) 177 | oauth2Group.POST("/token", func(c *gin.Context) { 178 | srv.HandleToken(c.Writer, c.Request) 179 | }) 180 | oauth2Group.POST("/device_authorization", func(c *gin.Context) { 181 | srv.HandleDeviceAuthorization(c.Writer, c.Request) 182 | }) 183 | } 184 | 185 | if err := http.ListenAndServe(":8003", r); err != nil { 186 | fmt.Printf("%+v\n", err) 187 | } 188 | } 189 | ``` 190 | 191 | # Client 192 | 193 | ```go 194 | package main 195 | 196 | import ( 197 | "github.com/gin-gonic/gin" 198 | "github.com/nilorg/oauth2" 199 | "github.com/nilorg/pkg/logger" 200 | ) 201 | 202 | var ( 203 | client *oauth2.Client 204 | ) 205 | 206 | func init() { 207 | logger.Init() 208 | client = oauth2.NewClient("http://localhost:8003", "oauth2_client", "password") 209 | client.Log = logger.Default() 210 | } 211 | func main() { 212 | r := gin.Default() 213 | r.GET("/ping", func(c *gin.Context) { 214 | //err := client.AuthorizeImplicit(c.Writer, "http://localhost:8080/callback", "test", "aaaaa") 215 | //if err != nil { 216 | // logger.Errorln(err) 217 | // return 218 | //} 219 | err := client.AuthorizeAuthorizationCode(c.Writer, "http://localhost:8080/callback", "test", "bbbbb") 220 | if err != nil { 221 | logger.Errorln(err) 222 | return 223 | } 224 | }) 225 | r.GET("/callback", func(c *gin.Context) { 226 | code := c.Query("code") 227 | state := c.Query("state") 228 | token, err := client.TokenAuthorizationCode(code, c.Request.URL.String(), state) 229 | if err != nil { 230 | c.JSON(200, gin.H{ 231 | "message": "callback", 232 | "err": err.Error(), 233 | }) 234 | } else { 235 | c.JSON(200, gin.H{ 236 | "message": "callback", 237 | "token": token, 238 | }) 239 | } 240 | }) 241 | 242 | r.Run() // listen and serve on 0.0.0.0:8080 243 | } 244 | ``` 245 | 246 | # jwt playload 247 | 248 | 标准中注册的声明 (建议但不强制使用) : 249 | 250 | `iss`: 令牌**颁发者**。它表示**该令牌是由谁创建的**,在好很多OAuth部署中会将它设为授权服务器的URL。该声明是一个字符串 251 | 252 | `sub`: 令牌的**主体**。它表示**该令牌是关于谁的**,在很多OAuth部署中会将它设为资源拥有者的唯一标识。在大多数情况下,主题在同一个颁发者的范围内必须是唯一的。该声明是一个字符串 253 | 254 | `aud`: 令牌的**受众**。它表示**令牌的接收者**,在很多OAuth部署中,它包含受保护资源的URI或者能够接收该令牌的受保护资源。该声明可以是一个字符串数组,如果只有一个值,也可以是一个不用数组包装的单个字符串 255 | 256 | `exp`: 令牌的**过期**时间戳。它表示**令牌将在何时过期**,以便部署应用让令牌自行失效。该声明是一个整数,表示自UNIX新世纪(即格林威治标准时间GMT,1970年1月1日零点)以来的秒数 257 | 258 | `nbf`: 令牌的**生效**时的时间戳。它表示**令牌从什么时候开始生效**,以便部署应用可以在令牌生效之前颁发令牌。该声明是一个整数,表示自UNIX新世纪(即格林威治标准时间GMT,1970年1月1日零点)以来的秒数 259 | 260 | `iat`: 令牌**颁发时**的时间戳。它表示**令牌是何时被创建的**,它通常是颁发者在生成令牌时的系统时间戳。该声明是一个整数,表示自UNIX新世纪(即格林威治标准时间GMT,1970年1月1日零点)以来的秒数 261 | 262 | `jti`: 令牌的**唯一标识符**。该声明的值**在令牌颁发者创建的每个令牌中都是唯一的**,为了防止冲突,它通常是一个密码学随机值。这个值相当于向结构化令牌中加入了一个攻击者无法获得的随机熵组件,有利于防止令牌猜测攻击和重放攻击 263 | 264 | --- 265 | 266 | 公共的声明 : 267 | 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密. 268 | 269 | 私有的声明 : 270 | 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。 -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 2 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 3 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 4 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 5 | github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s= 6 | github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= 7 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 8 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 9 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 10 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= 16 | github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= 17 | github.com/evalphobia/logrus_sentry v0.8.2 h1:dotxHq+YLZsT1Bb45bB5UQbfCh3gM/nFFetyN46VoDQ= 18 | github.com/evalphobia/logrus_sentry v0.8.2/go.mod h1:pKcp+vriitUqu9KiWj/VRFbRfFNUwz95/UkgG8a6MNc= 19 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 20 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 21 | github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= 22 | github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= 23 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 24 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 25 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 26 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 27 | github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= 28 | github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 29 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 30 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 31 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 32 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 33 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 34 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 35 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 36 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 37 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 38 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 39 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 40 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 41 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 42 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 43 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 44 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 45 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 46 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 47 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 48 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 49 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 50 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 51 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 52 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 53 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 54 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 55 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 56 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 57 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 58 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 59 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 60 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 61 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 62 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 63 | github.com/nilorg/pkg v0.0.0-20251216130642-faa5836f8085 h1:/Q6lKH6HXrnoY/ftjoLJwlGmbp/MXtrYhlXisr42OWs= 64 | github.com/nilorg/pkg v0.0.0-20251216130642-faa5836f8085/go.mod h1:gd0jjR4fn+9qFO47NEvFKdlsEUWJ0OclO1ex59Vh41s= 65 | github.com/nilorg/sdk v0.0.0-20251216121711-27713f2bd609 h1:J0V/I+2AuWIrF5ZTyPlQEsP6VXmH8aTtJCWxtPh6GOM= 66 | github.com/nilorg/sdk v0.0.0-20251216121711-27713f2bd609/go.mod h1:X1swpPdqguAZaBDoEPyEWHSsJii0YQ1o+3piMv6W3JU= 67 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 68 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 69 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 70 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 71 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 72 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 73 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 74 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 75 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 76 | github.com/sirupsen/logrus v1.8.3 h1:DBBfY8eMYazKEJHb3JKpSPfpgd2mBCoNFlQx6C5fftU= 77 | github.com/sirupsen/logrus v1.8.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 78 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 79 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 80 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 81 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 82 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 84 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 85 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 86 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 87 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 88 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 89 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 90 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 91 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 92 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 93 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 94 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 95 | golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= 96 | golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= 97 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 98 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 99 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 100 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 103 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 104 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 105 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 106 | google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= 107 | google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 108 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 109 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 110 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 111 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 112 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 113 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 114 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 115 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 116 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 117 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 118 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | ) 11 | 12 | // Client OAuth2客户端 / OAuth2 client for making authorization requests 13 | type Client struct { 14 | Log Logger // 日志记录器 / Logger instance 15 | httpClient *http.Client // HTTP客户端 / HTTP client for requests 16 | ServerBaseURL string // 服务器基础URL / OAuth2 server base URL 17 | AuthorizationEndpoint string // 授权端点 / Authorization endpoint path 18 | TokenEndpoint string // 令牌端点 / Token endpoint path 19 | IntrospectEndpoint string // 内省端点 / Introspection endpoint path 20 | DeviceAuthorizationEndpoint string // 设备授权端点 / Device authorization endpoint path 21 | TokenRevocationEndpoint string // 令牌撤销端点 / Token revocation endpoint path 22 | ID string // 客户端ID / Client identifier 23 | Secret string // 客户端密钥 / Client secret 24 | } 25 | 26 | // NewClient 创建OAuth2客户端 / Create a new OAuth2 client 27 | // serverBaseURL: 服务器基础URL / OAuth2 server base URL 28 | // id: 客户端ID / Client identifier 29 | // secret: 客户端密钥 / Client secret 30 | func NewClient(serverBaseURL, id, secret string) *Client { 31 | httpclient := &http.Client{} 32 | httpclient.CheckRedirect = func(req *http.Request, via []*http.Request) error { 33 | return http.ErrUseLastResponse 34 | } 35 | return &Client{ 36 | Log: &DefaultLogger{}, 37 | httpClient: httpclient, 38 | ServerBaseURL: serverBaseURL, 39 | AuthorizationEndpoint: "/authorize", 40 | TokenEndpoint: "/token", 41 | DeviceAuthorizationEndpoint: "/device_authorization", 42 | IntrospectEndpoint: "/introspect", 43 | ID: id, 44 | Secret: secret, 45 | } 46 | } 47 | 48 | func (c *Client) authorize(ctx context.Context, w http.ResponseWriter, responseType, redirectURI, scope, state string) (err error) { 49 | var uri *url.URL 50 | uri, err = url.Parse(c.ServerBaseURL + c.AuthorizationEndpoint) 51 | if err != nil { 52 | return 53 | } 54 | query := uri.Query() 55 | query.Set(ResponseTypeKey, responseType) 56 | query.Set(ClientIDKey, c.ID) 57 | query.Set(RedirectURIKey, redirectURI) 58 | query.Set(ScopeKey, scope) 59 | query.Set(StateKey, state) 60 | uri.RawQuery = query.Encode() 61 | var req *http.Request 62 | req, err = http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) 63 | if err != nil { 64 | return 65 | } 66 | var resp *http.Response 67 | resp, err = c.httpClient.Do(req) 68 | if err != nil { 69 | return 70 | } 71 | w.Header().Set("Location", resp.Header.Get("Location")) 72 | w.WriteHeader(resp.StatusCode) 73 | defer resp.Body.Close() 74 | _, err = io.Copy(w, resp.Body) 75 | return 76 | } 77 | 78 | // AuthorizeAuthorizationCode 授权码模式授权请求 / Authorization code grant authorization request 79 | // redirectURI: 重定向URI / Redirect URI after authorization 80 | // scope: 授权范围 / Requested scope 81 | // state: 状态码,用于防止CSRF攻击 / State parameter for CSRF protection 82 | func (c *Client) AuthorizeAuthorizationCode(ctx context.Context, w http.ResponseWriter, redirectURI, scope, state string) (err error) { 83 | return c.authorize(ctx, w, CodeKey, redirectURI, scope, state) 84 | } 85 | 86 | // TokenAuthorizationCode 授权码模式获取令牌 / Exchange authorization code for access token 87 | // code: 授权码 / Authorization code received from authorization server 88 | // redirectURI: 重定向URI / Redirect URI used in authorization request 89 | // clientID: 客户端ID / Client identifier 90 | func (c *Client) TokenAuthorizationCode(ctx context.Context, code, redirectURI, clientID string) (token *TokenResponse, err error) { 91 | values := url.Values{ 92 | CodeKey: []string{code}, 93 | RedirectURIKey: []string{redirectURI}, 94 | ClientIDKey: []string{clientID}, 95 | } 96 | return c.token(ctx, AuthorizationCodeKey, values) 97 | } 98 | 99 | // AuthorizeImplicit 隐式授权模式授权请求 / Implicit grant authorization request 100 | // redirectURI: 重定向URI / Redirect URI after authorization 101 | // scope: 授权范围 / Requested scope 102 | // state: 状态码,用于防止CSRF攻击 / State parameter for CSRF protection 103 | func (c *Client) AuthorizeImplicit(ctx context.Context, w http.ResponseWriter, redirectURI, scope, state string) (err error) { 104 | return c.authorize(ctx, w, TokenKey, redirectURI, scope, state) 105 | } 106 | 107 | // DeviceAuthorization 设备授权请求 / Device authorization request (RFC 8628) 108 | // scope: 授权范围 / Requested scope 109 | func (c *Client) DeviceAuthorization(ctx context.Context, w http.ResponseWriter, scope string) (err error) { 110 | var uri *url.URL 111 | uri, err = url.Parse(c.ServerBaseURL + c.DeviceAuthorizationEndpoint) 112 | if err != nil { 113 | return 114 | } 115 | query := uri.Query() 116 | query.Set(ClientIDKey, c.ID) 117 | query.Set(ScopeKey, scope) 118 | uri.RawQuery = query.Encode() 119 | var req *http.Request 120 | req, err = http.NewRequestWithContext(ctx, http.MethodGet, uri.String(), nil) 121 | if err != nil { 122 | return 123 | } 124 | var resp *http.Response 125 | resp, err = c.httpClient.Do(req) 126 | if err != nil { 127 | return 128 | } 129 | w.WriteHeader(resp.StatusCode) 130 | defer resp.Body.Close() 131 | _, err = io.Copy(w, resp.Body) 132 | return 133 | } 134 | 135 | func (c *Client) Token(ctx context.Context, grantType string, values url.Values) (token *TokenResponse, err error) { 136 | return c.token(ctx, grantType, values) 137 | } 138 | 139 | func (c *Client) token(ctx context.Context, grantType string, values url.Values) (token *TokenResponse, err error) { 140 | var uri *url.URL 141 | uri, err = url.Parse(c.ServerBaseURL + c.TokenEndpoint) 142 | if err != nil { 143 | return 144 | } 145 | if values == nil { 146 | values = url.Values{ 147 | GrantTypeKey: []string{grantType}, 148 | } 149 | } else { 150 | values.Set(GrantTypeKey, grantType) 151 | } 152 | var req *http.Request 153 | req, err = http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), strings.NewReader(values.Encode())) 154 | if err != nil { 155 | return 156 | } 157 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 158 | 159 | // explain: https://tools.ietf.org/html/rfc8628#section-3.4 160 | if grantType != DeviceCodeKey && grantType != UrnIetfParamsOAuthGrantTypeDeviceCodeKey { 161 | req.SetBasicAuth(c.ID, c.Secret) 162 | } 163 | 164 | var resp *http.Response 165 | resp, err = c.httpClient.Do(req) 166 | if err != nil { 167 | return 168 | } 169 | defer resp.Body.Close() 170 | var body []byte 171 | body, err = io.ReadAll(resp.Body) 172 | if err != nil { 173 | return 174 | } 175 | if resp.StatusCode != http.StatusOK || strings.Contains(string(body), ErrorKey) { 176 | errModel := ErrorResponse{} 177 | err = json.Unmarshal(body, &errModel) 178 | if err != nil { 179 | return 180 | } 181 | err = Errors[errModel.Error] 182 | } else { 183 | token = &TokenResponse{} 184 | err = json.Unmarshal(body, token) 185 | } 186 | return 187 | } 188 | 189 | // TokenResourceOwnerPasswordCredentials 密码模式获取令牌 / Resource owner password credentials grant 190 | // username: 用户名 / Resource owner username 191 | // password: 密码 / Resource owner password 192 | func (c *Client) TokenResourceOwnerPasswordCredentials(ctx context.Context, username, password string) (model *TokenResponse, err error) { 193 | values := url.Values{ 194 | UsernameKey: []string{username}, 195 | PasswordKey: []string{password}, 196 | } 197 | return c.token(ctx, PasswordKey, values) 198 | } 199 | 200 | // TokenClientCredentials 客户端凭证模式获取令牌 / Client credentials grant 201 | // scope: 授权范围(可选) / Requested scope (optional) 202 | func (c *Client) TokenClientCredentials(ctx context.Context, scope ...string) (model *TokenResponse, err error) { 203 | values := url.Values{} 204 | if len(scope) > 0 { 205 | values.Set(ScopeKey, scope[0]) 206 | } 207 | return c.token(ctx, ClientCredentialsKey, values) 208 | } 209 | 210 | // RefreshToken 刷新访问令牌 / Refresh access token using refresh token 211 | // refreshToken: 刷新令牌 / Refresh token 212 | func (c *Client) RefreshToken(ctx context.Context, refreshToken string) (model *TokenResponse, err error) { 213 | values := url.Values{ 214 | RefreshTokenKey: []string{refreshToken}, 215 | } 216 | return c.token(ctx, RefreshTokenKey, values) 217 | } 218 | 219 | // TokenDeviceCode 设备码模式获取令牌 / Exchange device code for access token (RFC 8628) 220 | // deviceCode: 设备码 / Device code received from device authorization 221 | func (c *Client) TokenDeviceCode(ctx context.Context, deviceCode string) (model *TokenResponse, err error) { 222 | values := url.Values{ 223 | ClientIDKey: []string{c.ID}, 224 | DeviceCodeKey: []string{deviceCode}, 225 | } 226 | return c.token(ctx, DeviceCodeKey, values) 227 | } 228 | 229 | // TokenIntrospect 令牌内省 / Token introspection (RFC 7662) 230 | // token: 要检查的令牌 / Token to introspect 231 | // tokenTypeHint: 令牌类型提示(可选) / Token type hint (optional): access_token or refresh_token 232 | func (c *Client) TokenIntrospect(ctx context.Context, token string, tokenTypeHint ...string) (introspection *IntrospectionResponse, err error) { 233 | values := url.Values{ 234 | TokenKey: []string{token}, 235 | } 236 | if len(tokenTypeHint) > 0 { 237 | if tokenTypeHint[0] != AccessTokenKey && tokenTypeHint[0] != RefreshTokenKey { 238 | err = ErrUnsupportedTokenType 239 | return 240 | } 241 | values.Set(TokenTypeHintKey, tokenTypeHint[0]) 242 | } 243 | introspection = &IntrospectionResponse{} 244 | err = c.do(ctx, c.IntrospectEndpoint, values, introspection) 245 | return 246 | } 247 | 248 | // TokenRevocation 令牌撤销 / Token revocation (RFC 7009) 249 | // token: 要撤销的令牌 / Token to revoke 250 | // tokenTypeHint: 令牌类型提示(可选) / Token type hint (optional): access_token or refresh_token 251 | func (c *Client) TokenRevocation(ctx context.Context, token string, tokenTypeHint ...string) (introspection *IntrospectionResponse, err error) { 252 | values := url.Values{ 253 | TokenKey: []string{token}, 254 | } 255 | if len(tokenTypeHint) > 0 { 256 | if tokenTypeHint[0] != AccessTokenKey && tokenTypeHint[0] != RefreshTokenKey { 257 | err = ErrUnsupportedTokenType 258 | return 259 | } 260 | values.Set(TokenTypeHintKey, tokenTypeHint[0]) 261 | } 262 | err = c.do(ctx, c.TokenRevocationEndpoint, values, nil) 263 | return 264 | } 265 | 266 | func (c *Client) do(ctx context.Context, path string, values url.Values, v interface{}) (err error) { 267 | var uri *url.URL 268 | uri, err = url.Parse(c.ServerBaseURL + path) 269 | if err != nil { 270 | return 271 | } 272 | var req *http.Request 273 | req, err = http.NewRequestWithContext(ctx, http.MethodPost, uri.String(), strings.NewReader(values.Encode())) 274 | if err != nil { 275 | return 276 | } 277 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 278 | req.SetBasicAuth(c.ID, c.Secret) 279 | var resp *http.Response 280 | resp, err = c.httpClient.Do(req) 281 | if err != nil { 282 | return 283 | } 284 | defer resp.Body.Close() 285 | var body []byte 286 | body, err = io.ReadAll(resp.Body) 287 | if err != nil { 288 | return 289 | } 290 | if resp.StatusCode != http.StatusOK || strings.Contains(string(body), ErrorKey) { 291 | errModel := ErrorResponse{} 292 | err = json.Unmarshal(body, &errModel) 293 | if err != nil { 294 | return 295 | } 296 | err = Errors[errModel.Error] 297 | } else { 298 | if v != nil { 299 | err = json.Unmarshal(body, v) 300 | } 301 | } 302 | return 303 | } 304 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package oauth2 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | ) 10 | 11 | // Server OAuth2Server 12 | type Server struct { 13 | VerifyClient VerifyClientFunc 14 | VerifyClientID VerifyClientIDFunc 15 | VerifyScope VerifyScopeFunc 16 | VerifyGrantType VerifyGrantTypeFunc 17 | VerifyPassword VerifyPasswordFunc 18 | VerifyRedirectURI VerifyRedirectURIFunc 19 | GenerateCode GenerateCodeFunc 20 | VerifyCode VerifyCodeFunc 21 | GenerateDeviceAuthorization GenerateDeviceAuthorizationFunc 22 | VerifyDeviceCode VerifyDeviceCodeFunc 23 | VerifyIntrospectionToken VerifyIntrospectionTokenFunc 24 | TokenRevocation TokenRevocationFunc 25 | opts ServerOptions 26 | AccessToken AccessTokener 27 | } 28 | 29 | // NewServer 创建服务器 30 | func NewServer(opts ...ServerOption) *Server { 31 | options := newServerOptions(opts...) 32 | return &Server{ 33 | opts: options, 34 | } 35 | } 36 | 37 | // Init 初始化服务器,验证必要的函数是否已设置,未设置则panic / Initialize server, panic if required functions are not set 38 | // 39 | // Deprecated: 推荐使用 InitWithError 方法,它返回错误而不是panic / Use InitWithError instead, which returns error instead of panic 40 | func (srv *Server) Init(opts ...ServerOption) { 41 | if err := srv.InitWithError(opts...); err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | // InitWithError 初始化服务器,验证必要的函数是否已设置,返回错误 / Initialize server, return error if required functions are not set 47 | func (srv *Server) InitWithError(opts ...ServerOption) error { 48 | for _, o := range opts { 49 | o(&srv.opts) 50 | } 51 | 52 | if srv.VerifyClient == nil { 53 | return ErrVerifyClientFuncNil 54 | } 55 | if srv.VerifyClientID == nil { 56 | return ErrVerifyClientIDFuncNil 57 | } 58 | if srv.VerifyPassword == nil { 59 | return ErrVerifyPasswordFuncNil 60 | } 61 | if srv.VerifyRedirectURI == nil { 62 | return ErrVerifyRedirectURIFuncNil 63 | } 64 | if srv.GenerateCode == nil { 65 | return ErrGenerateCodeFuncNil 66 | } 67 | if srv.VerifyCode == nil { 68 | return ErrVerifyCodeFuncNil 69 | } 70 | if srv.VerifyScope == nil { 71 | return ErrVerifyScopeFuncNil 72 | } 73 | if srv.VerifyGrantType == nil { 74 | return ErrVerifyGrantTypeFuncNil 75 | } 76 | if srv.AccessToken == nil { 77 | return ErrAccessToken 78 | } 79 | 80 | if srv.opts.DeviceAuthorizationEndpointEnabled { 81 | if srv.GenerateDeviceAuthorization == nil { 82 | return ErrGenerateDeviceAuthorizationFuncNil 83 | } 84 | if srv.VerifyDeviceCode == nil { 85 | return ErrVerifyDeviceCodeFuncNil 86 | } 87 | } 88 | if srv.opts.IntrospectEndpointEnabled { 89 | if srv.VerifyIntrospectionToken == nil { 90 | return ErrVerifyIntrospectionTokenFuncNil 91 | } 92 | } 93 | if srv.opts.TokenRevocationEnabled { 94 | if srv.TokenRevocation == nil { 95 | return ErrTokenRevocationFuncNil 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | // HandleAuthorize 处理Authorize 102 | func (srv *Server) HandleAuthorize(w http.ResponseWriter, r *http.Request) { 103 | ctx := r.Context() 104 | // 判断参数 105 | responseType := r.FormValue(ResponseTypeKey) 106 | clientID := r.FormValue(ClientIDKey) 107 | scope := r.FormValue(ScopeKey) 108 | state := r.FormValue(StateKey) 109 | redirectURIStr := r.FormValue(RedirectURIKey) 110 | redirectURI, err := url.Parse(redirectURIStr) 111 | if err != nil { 112 | WriterError(w, ErrInvalidRequest) 113 | return 114 | } 115 | if responseType == "" || clientID == "" { 116 | RedirectError(w, r, redirectURI, ErrInvalidRequest) 117 | return 118 | } 119 | 120 | switch responseType { 121 | case CodeKey: 122 | if err = srv.VerifyGrantType(ctx, clientID, AuthorizationCodeKey); err != nil { 123 | RedirectError(w, r, redirectURI, err) 124 | return 125 | } 126 | case TokenKey: 127 | if err = srv.VerifyGrantType(ctx, clientID, ImplicitKey); err != nil { 128 | RedirectError(w, r, redirectURI, err) 129 | return 130 | } 131 | } 132 | 133 | err = srv.VerifyRedirectURI(ctx, clientID, redirectURI.String()) 134 | if err != nil { 135 | RedirectError(w, r, redirectURI, err) 136 | return 137 | } 138 | 139 | if err = srv.VerifyScope(ctx, StringSplit(scope, " "), clientID); err != nil { 140 | // ErrInvalidScope 141 | RedirectError(w, r, redirectURI, err) 142 | return 143 | } 144 | var openID string 145 | openID, err = OpenIDFromContext(r.Context()) 146 | if err != nil { 147 | RedirectError(w, r, redirectURI, ErrServerError) 148 | return 149 | } 150 | switch responseType { 151 | case CodeKey: 152 | var code string 153 | code, err = srv.authorizeAuthorizationCode(ctx, clientID, redirectURIStr, scope, openID) 154 | if err != nil { 155 | RedirectError(w, r, redirectURI, err) 156 | } else { 157 | RedirectSuccess(w, r, redirectURI, code) 158 | } 159 | case TokenKey: 160 | var token *TokenResponse 161 | token, err = srv.authorizeImplicit(ctx, clientID, scope, openID) 162 | if err != nil { 163 | RedirectError(w, r, redirectURI, err) 164 | } else { 165 | http.Redirect(w, r, fmt.Sprintf("%s#access_token=%s&refresh_token=%s&id_token=%s&state=%s&token_type=%s&expires_in=%d", redirectURIStr, token.AccessToken, token.RefreshToken, token.IDToken, state, token.TokenType, token.ExpiresIn), http.StatusFound) 166 | } 167 | default: 168 | RedirectError(w, r, redirectURI, ErrUnsupportedResponseType) 169 | } 170 | } 171 | 172 | // HandleDeviceAuthorization 处理DeviceAuthorization 173 | // https://tools.ietf.org/html/rfc8628#section-3.1 174 | func (srv *Server) HandleDeviceAuthorization(w http.ResponseWriter, r *http.Request) { 175 | ctx := r.Context() 176 | // 判断参数 177 | clientID := r.FormValue(ClientIDKey) 178 | if clientID == "" { 179 | WriterError(w, ErrInvalidRequest) 180 | return 181 | } 182 | if err := srv.VerifyClientID(ctx, clientID); err != nil { 183 | WriterError(w, err) 184 | return 185 | } 186 | if err := srv.VerifyGrantType(ctx, clientID, DeviceCodeKey); err != nil { 187 | WriterError(w, err) 188 | return 189 | } 190 | scope := r.FormValue(ScopeKey) 191 | if err := srv.VerifyScope(ctx, StringSplit(scope, " "), clientID); err != nil { 192 | WriterError(w, err) 193 | return 194 | } 195 | resp, err := srv.authorizeDeviceCode(ctx, clientID, scope) 196 | if err != nil { 197 | WriterError(w, err) 198 | } else { 199 | WriterJSON(w, resp) 200 | } 201 | } 202 | 203 | // HandleTokenIntrospection 处理内省端点 204 | // https://tools.ietf.org/html/rfc7662#section-2.1 205 | func (srv *Server) HandleTokenIntrospection(w http.ResponseWriter, r *http.Request) { 206 | ctx := r.Context() 207 | var reqClientBasic *ClientBasic 208 | var err error 209 | reqClientBasic, err = RequestClientBasic(r) 210 | if err != nil { 211 | WriterError(w, err) 212 | return 213 | } 214 | err = srv.VerifyClient(ctx, reqClientBasic) 215 | if err != nil { 216 | WriterError(w, err) 217 | return 218 | } 219 | 220 | token := r.FormValue(TokenKey) 221 | tokenTypeHint := r.FormValue(TokenTypeHintKey) 222 | if tokenTypeHint != "" && tokenTypeHint != AccessTokenKey && tokenTypeHint != RefreshTokenKey { 223 | WriterError(w, ErrUnsupportedTokenType) 224 | return 225 | } 226 | var resp *IntrospectionResponse 227 | resp, err = srv.VerifyIntrospectionToken(ctx, token, reqClientBasic.ID, tokenTypeHint) 228 | if err != nil { 229 | WriterError(w, err) 230 | } else { 231 | WriterJSON(w, resp) 232 | } 233 | } 234 | 235 | // HandleTokenRevocation 处理Token销毁 236 | // https://tools.ietf.org/html/rfc7009 237 | func (srv *Server) HandleTokenRevocation(w http.ResponseWriter, r *http.Request) { 238 | ctx := r.Context() 239 | var reqClientBasic *ClientBasic 240 | var err error 241 | reqClientBasic, err = RequestClientBasic(r) 242 | if err != nil { 243 | WriterError(w, err) 244 | return 245 | } 246 | err = srv.VerifyClient(ctx, reqClientBasic) 247 | if err != nil { 248 | WriterError(w, err) 249 | return 250 | } 251 | 252 | // 判断参数 253 | clientID := r.FormValue(ClientIDKey) 254 | if clientID == "" { 255 | WriterError(w, ErrInvalidRequest) 256 | return 257 | } 258 | if reqClientBasic.ID != clientID { 259 | WriterError(w, ErrInvalidRequest) 260 | return 261 | } 262 | token := r.FormValue(TokenKey) 263 | tokenTypeHint := r.FormValue(TokenTypeHintKey) 264 | if tokenTypeHint != "" && tokenTypeHint != AccessTokenKey && tokenTypeHint != RefreshTokenKey { 265 | WriterError(w, ErrUnsupportedTokenType) 266 | return 267 | } 268 | srv.TokenRevocation(ctx, token, clientID, tokenTypeHint) 269 | w.WriteHeader(http.StatusOK) 270 | } 271 | 272 | // HandleToken 处理Token 273 | func (srv *Server) HandleToken(w http.ResponseWriter, r *http.Request) { 274 | ctx := r.Context() 275 | grantType := r.PostFormValue(GrantTypeKey) 276 | if grantType == "" { 277 | WriterError(w, ErrInvalidRequest) 278 | return 279 | } 280 | 281 | var reqClientBasic *ClientBasic 282 | var err error 283 | var clientID string 284 | // explain: https://tools.ietf.org/html/rfc8628#section-3.4 { 285 | if grantType != DeviceCodeKey && grantType != UrnIetfParamsOAuthGrantTypeDeviceCodeKey { 286 | reqClientBasic, err = RequestClientBasic(r) 287 | if err != nil { 288 | WriterError(w, err) 289 | return 290 | } 291 | err = srv.VerifyClient(ctx, reqClientBasic) 292 | if err != nil { 293 | WriterError(w, err) 294 | return 295 | } 296 | clientID = reqClientBasic.ID 297 | } else { 298 | clientID = r.PostFormValue(ClientIDKey) 299 | err = srv.VerifyClientID(ctx, clientID) 300 | if err != nil { 301 | WriterError(w, err) 302 | return 303 | } 304 | } 305 | 306 | vgrantType := grantType 307 | if vgrantType == UrnIetfParamsOAuthGrantTypeDeviceCodeKey { 308 | vgrantType = DeviceCodeKey 309 | } 310 | err = srv.VerifyGrantType(ctx, clientID, vgrantType) 311 | if err != nil { 312 | WriterError(w, err) 313 | return 314 | } 315 | 316 | scope := r.PostFormValue(ScopeKey) 317 | if err = srv.VerifyScope(ctx, StringSplit(scope, " "), clientID); err != nil { 318 | // ErrInvalidScope 319 | WriterError(w, err) 320 | return 321 | } 322 | 323 | switch grantType { 324 | case RefreshTokenKey: 325 | refreshToken := r.PostFormValue(RefreshTokenKey) 326 | model, err := srv.AccessToken.Refresh(ctx, reqClientBasic.ID, refreshToken) 327 | if err != nil { 328 | WriterError(w, err) 329 | } else { 330 | WriterJSON(w, model) 331 | } 332 | case AuthorizationCodeKey: 333 | code := r.PostFormValue(CodeKey) 334 | redirectURIStr := r.PostFormValue(RedirectURIKey) 335 | if clientID == "" { 336 | clientID = r.PostFormValue(ClientIDKey) 337 | } 338 | if code == "" || redirectURIStr == "" || clientID == "" { 339 | WriterError(w, ErrInvalidRequest) 340 | return 341 | } 342 | var model *TokenResponse 343 | model, err = srv.tokenAuthorizationCode(ctx, reqClientBasic, clientID, code, redirectURIStr) 344 | if err != nil { 345 | WriterError(w, err) 346 | } else { 347 | WriterJSON(w, model) 348 | } 349 | case PasswordKey: 350 | username := r.PostFormValue(UsernameKey) 351 | password := r.PostFormValue(PasswordKey) 352 | if username == "" || password == "" { 353 | WriterError(w, ErrInvalidRequest) 354 | return 355 | } 356 | var model *TokenResponse 357 | model, err := srv.tokenResourceOwnerPasswordCredentials(ctx, reqClientBasic, username, password, scope) 358 | if err != nil { 359 | WriterError(w, err) 360 | } else { 361 | WriterJSON(w, model) 362 | } 363 | case ClientCredentialsKey: 364 | model, err := srv.tokenClientCredentials(ctx, reqClientBasic, scope) 365 | if err != nil { 366 | WriterError(w, err) 367 | } else { 368 | WriterJSON(w, model) 369 | } 370 | case UrnIetfParamsOAuthGrantTypeDeviceCodeKey, DeviceCodeKey: // https://tools.ietf.org/html/rfc8628#section-3.4 371 | deviceCode := r.PostFormValue(DeviceCodeKey) 372 | clientID := r.PostFormValue(ClientIDKey) 373 | model, err := srv.tokenDeviceCode(ctx, clientID, deviceCode) 374 | if err != nil { 375 | WriterError(w, err) 376 | } else { 377 | WriterJSON(w, model) 378 | } 379 | default: 380 | if srv.opts.CustomGrantTypeEnabled { 381 | custom, ok := srv.opts.CustomGrantTypeAuthentication[grantType] 382 | if ok { 383 | model, err := srv.generateCustomGrantTypeAccessToken(ctx, reqClientBasic, scope, r, custom) 384 | if err != nil { 385 | WriterError(w, err) 386 | } else { 387 | WriterJSON(w, model) 388 | } 389 | return 390 | } 391 | } 392 | WriterError(w, ErrUnsupportedGrantType) 393 | } 394 | } 395 | 396 | // 授权码(authorization-code) 397 | func (srv *Server) authorizeAuthorizationCode(ctx context.Context, clientID, redirectURI, scope, openID string) (code string, err error) { 398 | return srv.GenerateCode(ctx, clientID, openID, redirectURI, StringSplit(scope, " ")) 399 | } 400 | 401 | func (srv *Server) tokenAuthorizationCode(ctx context.Context, client *ClientBasic, clientID, code, redirectURI string) (token *TokenResponse, err error) { 402 | if client.ID != clientID { 403 | err = ErrInvalidClient 404 | return 405 | } 406 | var value *CodeValue 407 | value, err = srv.VerifyCode(ctx, code, client.ID, redirectURI) 408 | if err != nil { 409 | return 410 | } 411 | scope := strings.Join(value.Scope, " ") 412 | token, err = srv.AccessToken.Generate(ctx, srv.opts.Issuer, client.ID, scope, value.OpenID, value) 413 | return 414 | } 415 | 416 | // 隐藏式(implicit) 417 | func (srv *Server) authorizeImplicit(ctx context.Context, clientID, scope, openID string) (token *TokenResponse, err error) { 418 | token, err = srv.AccessToken.Generate(ctx, srv.opts.Issuer, clientID, scope, openID, nil) 419 | return 420 | } 421 | 422 | // 设备模式(Device Code) 423 | func (srv *Server) authorizeDeviceCode(ctx context.Context, clientID, scope string) (resp *DeviceAuthorizationResponse, err error) { 424 | resp, err = srv.GenerateDeviceAuthorization(ctx, srv.opts.Issuer, srv.opts.DeviceVerificationURI, clientID, StringSplit(scope, " ")) 425 | return 426 | } 427 | 428 | // 密码式(password) 429 | func (srv *Server) tokenResourceOwnerPasswordCredentials(ctx context.Context, client *ClientBasic, username, password, scope string) (token *TokenResponse, err error) { 430 | var openID string 431 | openID, err = srv.VerifyPassword(ctx, client.ID, username, password) 432 | if err != nil { 433 | return 434 | } 435 | token, err = srv.AccessToken.Generate(ctx, srv.opts.Issuer, client.ID, scope, openID, nil) 436 | return 437 | } 438 | 439 | // generateCustomGrantTypeAccessToken 生成自定义GrantType Token 440 | func (srv *Server) generateCustomGrantTypeAccessToken(ctx context.Context, client *ClientBasic, scope string, req *http.Request, custom CustomGrantTypeAuthenticationFunc) (token *TokenResponse, err error) { 441 | var openID string 442 | openID, err = custom(ctx, client, req) 443 | if err != nil { 444 | return 445 | } 446 | token, err = srv.AccessToken.Generate(ctx, srv.opts.Issuer, client.ID, scope, openID, nil) 447 | return 448 | } 449 | 450 | // 客户端凭证(client credentials) 451 | func (srv *Server) tokenClientCredentials(ctx context.Context, client *ClientBasic, scope string) (token *TokenResponse, err error) { 452 | token, err = srv.AccessToken.Generate(ctx, srv.opts.Issuer, client.ID, scope, "", nil) 453 | return 454 | } 455 | 456 | // 设备模式(Device Code) 457 | func (srv *Server) tokenDeviceCode(ctx context.Context, clientID, deviceCode string) (token *TokenResponse, err error) { 458 | var value *DeviceCodeValue 459 | value, err = srv.VerifyDeviceCode(ctx, deviceCode, clientID) 460 | if err != nil { 461 | return 462 | } 463 | scope := strings.Join(value.Scope, " ") 464 | token, err = srv.AccessToken.Generate(ctx, srv.opts.Issuer, clientID, scope, value.OpenID, nil) 465 | return 466 | } 467 | --------------------------------------------------------------------------------