diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..d8acfbe --- /dev/null +++ b/NOTICE @@ -0,0 +1,10 @@ +xslasd/x-oidc +Copyright 2023 xslasd +This product includes software developed by the Apache Software Foundation (http://www.apache.org/). + +This product includes software developed by go-jose (github.com/go-jose/go-jose/v3). +This product includes software developed by google/uuid (github.com/google/uuid). + +This project referred to the redesign and implementation of interface functions for zitadel/oidc. + +The above code files or parts of them are licensed under the Apache 2.0 License and are subject to the terms and conditions of the Apache 2.0 License. \ No newline at end of file diff --git a/README.md b/README.md index b2fa94c..8036ae3 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,14 @@ op.go definition and implementation of an OIDC OpenID Provider (server) ## Third-party Library The library primarily depends on the third-party library "go-jose/v3". -The HTTP processing section uses an interface-based approach (with net/http being the default), which can be extended as needed. +The HTTP processing section uses an interface-based approach , which can be extended as needed. +When starting OP, implement Config.OpenIDWrapper. By default, github. com/xslass/x-oidc/example/server/httpwrapper can be used. Implementation based on net/HTTP. ``` github.com/go-jose/go-jose/v3 v3.0.0 github.com/google/uuid v1.3.0 golang.org/x/text v0.9.0 ``` +Special thanks to [zitadel/oidc](https://github.com/zitadel/oidc). This project referred to the redesign and implementation of interface functions for zitadel/oidc. ## Contributors diff --git a/config.go b/config.go index 2ba4693..7e42df2 100644 --- a/config.go +++ b/config.go @@ -1,13 +1,13 @@ package oidc import ( - "github.com/xslasd/x-oidc/crypto" "github.com/xslasd/x-oidc/storage" + "github.com/xslasd/x-oidc/util" ) type Config struct { - Issuer string - Crypto crypto.JWTCertifier - Handler OpenIDHandler - Storage storage.IStorage + Issuer string + Crypto util.JWTCertifier + OpenIDWrapper OpenIDWrapper + Storage storage.IStorage } diff --git a/crypto/crypto.go b/crypto/crypto.go deleted file mode 100644 index 109fa0b..0000000 --- a/crypto/crypto.go +++ /dev/null @@ -1,69 +0,0 @@ -package crypto - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/base64" - "errors" - "io" -) - -var ErrCipherTextBlockSize = errors.New("ciphertext block size is too short") - -func EncryptAES(data string, key string) (string, error) { - encrypted, err := EncryptBytesAES([]byte(data), key) - if err != nil { - return "", err - } - - return base64.RawURLEncoding.EncodeToString(encrypted), nil -} - -func EncryptBytesAES(plainText []byte, key string) ([]byte, error) { - block, err := aes.NewCipher([]byte(key)) - if err != nil { - return nil, err - } - - cipherText := make([]byte, aes.BlockSize+len(plainText)) - iv := cipherText[:aes.BlockSize] - if _, err = io.ReadFull(rand.Reader, iv); err != nil { - return nil, err - } - - stream := cipher.NewCFBEncrypter(block, iv) - stream.XORKeyStream(cipherText[aes.BlockSize:], plainText) - - return cipherText, nil -} - -func DecryptAES(data string, key string) (string, error) { - text, err := base64.RawURLEncoding.DecodeString(data) - if err != nil { - return "", err - } - decrypted, err := DecryptBytesAES(text, key) - if err != nil { - return "", err - } - return string(decrypted), nil -} - -func DecryptBytesAES(cipherText []byte, key string) ([]byte, error) { - block, err := aes.NewCipher([]byte(key)) - if err != nil { - return nil, err - } - - if len(cipherText) < aes.BlockSize { - return nil, ErrCipherTextBlockSize - } - iv := cipherText[:aes.BlockSize] - cipherText = cipherText[aes.BlockSize:] - - stream := cipher.NewCFBDecrypter(block, iv) - stream.XORKeyStream(cipherText, cipherText) - - return cipherText, err -} diff --git a/crypto/hash.go b/crypto/hash.go deleted file mode 100644 index 504b42b..0000000 --- a/crypto/hash.go +++ /dev/null @@ -1,44 +0,0 @@ -package crypto - -import ( - "crypto/sha256" - "crypto/sha512" - "encoding/base64" - "errors" - "fmt" - "github.com/go-jose/go-jose/v3" - "hash" -) - -var ErrUnsupportedAlgorithm = errors.New("unsupported signing algorithm") - -func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) { - switch sigAlgorithm { - case jose.RS256, jose.ES256, jose.PS256, jose.HS256: - return sha256.New(), nil - case jose.RS384, jose.ES384, jose.PS384, jose.HS384: - return sha512.New384(), nil - case jose.RS512, jose.ES512, jose.PS512, jose.HS512: - return sha512.New(), nil - default: - return nil, fmt.Errorf("%w: %q", ErrUnsupportedAlgorithm, sigAlgorithm) - } -} - -func ClaimHash(claim string, sigAlgorithm jose.SignatureAlgorithm) (string, error) { - hash, err := GetHashAlgorithm(sigAlgorithm) - if err != nil { - return "", err - } - return HashString(hash, claim, true), nil -} - -func HashString(hash hash.Hash, s string, firstHalf bool) string { - hash.Write([]byte(s)) - size := hash.Size() - if firstHalf { - size = size / 2 - } - sum := hash.Sum(nil)[:size] - return base64.RawURLEncoding.EncodeToString(sum) -} diff --git a/ecode/oidc_error.go b/ecode/oidc_error.go index dc6317a..ed9908f 100644 --- a/ecode/oidc_error.go +++ b/ecode/oidc_error.go @@ -53,6 +53,7 @@ var ( UnauthorizedClientGrantType = New(1031, UnsupportedGrantTypeErrorType, "The grantType '%s' unsupported", "") AuthReqNotDone = New(1032, InvalidRequestErrorType, "Unfortunately, the user may be not logged in and/or additional interaction is required.", "") - PublicKeyInvalid = New(1050, ServerErrorErrorType, "failed to decode PEM block containing public key", "") - PrivateKeyInvalid = New(1051, ServerErrorErrorType, "failed to decode PEM block containing private key", "") + PublicKeyInvalid = New(1050, ServerErrorErrorType, "failed to decode PEM block containing public key", "") + PrivateKeyInvalid = New(1051, ServerErrorErrorType, "failed to decode PEM block containing private key", "") + AlgorithmUnsupported = New(1052, ServerErrorErrorType, "unsupported jose signing algorithm: %s", "") ) diff --git a/example/server/handler/httphandler.go b/example/server/httpwrapper/httphandler.go similarity index 93% rename from example/server/handler/httphandler.go rename to example/server/httpwrapper/httphandler.go index 5fa56fc..e422474 100644 --- a/example/server/handler/httphandler.go +++ b/example/server/httpwrapper/httphandler.go @@ -1,4 +1,4 @@ -package handler +package httpwrapper import ( "context" @@ -15,20 +15,20 @@ import ( "time" ) -type HttpHandler struct { +type HttpWrapper struct { handler *http.ServeMux addr string logger log.Logger } -func NewHttpHandler(addr string) *HttpHandler { - return &HttpHandler{handler: http.DefaultServeMux, addr: addr} +func NewHttpHandler(addr string) *HttpWrapper { + return &HttpWrapper{handler: http.DefaultServeMux, addr: addr} } -func (h *HttpHandler) SetLogger(logger log.Logger) { +func (h *HttpWrapper) SetLogger(logger log.Logger) { h.logger = logger } -func (h *HttpHandler) ListenAndServe() error { +func (h *HttpWrapper) ListenAndServe() error { h.login() var err error srv := &http.Server{ @@ -60,7 +60,7 @@ func (h *HttpHandler) ListenAndServe() error { } } -func (h *HttpHandler) DiscoveryJWKs(jwksEndpoint string, handler func() (*jose.JSONWebKeySet, error)) { +func (h *HttpWrapper) DiscoveryJWKs(jwksEndpoint string, handler func() (*jose.JSONWebKeySet, error)) { h.handler.HandleFunc(jwksEndpoint, func(w http.ResponseWriter, r *http.Request) { data, err := handler() if err != nil { @@ -74,7 +74,7 @@ func (h *HttpHandler) DiscoveryJWKs(jwksEndpoint string, handler func() (*jose.J }) } -func (h *HttpHandler) DiscoveryConfig(discoveryEndpoint string, handler func(req *x_oidc.DiscoveryConfigReq) *model.DiscoveryConfiguration) { +func (h *HttpWrapper) DiscoveryConfig(discoveryEndpoint string, handler func(req *x_oidc.DiscoveryConfigReq) *model.DiscoveryConfiguration) { h.handler.HandleFunc(discoveryEndpoint, func(w http.ResponseWriter, r *http.Request) { data := handler(&x_oidc.DiscoveryConfigReq{ RegistrationEndpoint: "", @@ -88,7 +88,7 @@ func (h *HttpHandler) DiscoveryConfig(discoveryEndpoint string, handler func(req }) } -func (h *HttpHandler) Authorize(authorizationEndpoint string, handler func(ctx context.Context, req *x_oidc.AuthRequestReq) (string, error)) { +func (h *HttpWrapper) Authorize(authorizationEndpoint string, handler func(ctx context.Context, req *x_oidc.AuthRequestReq) (string, error)) { h.handler.HandleFunc(authorizationEndpoint, func(w http.ResponseWriter, r *http.Request) { var authRequestReq x_oidc.AuthRequestReq if r.Method == "GET" { @@ -136,7 +136,7 @@ func (h *HttpHandler) Authorize(authorizationEndpoint string, handler func(ctx c }) } -func (h *HttpHandler) EndSession(endSessionEndpoint string, handler func(ctx context.Context, req *x_oidc.EndSessionReq) (string, error)) { +func (h *HttpWrapper) EndSession(endSessionEndpoint string, handler func(ctx context.Context, req *x_oidc.EndSessionReq) (string, error)) { h.handler.HandleFunc(endSessionEndpoint, func(w http.ResponseWriter, r *http.Request) { var endSessionReq x_oidc.EndSessionReq r.ParseForm() @@ -168,7 +168,7 @@ func (h *HttpHandler) EndSession(endSessionEndpoint string, handler func(ctx con }) } -func (h *HttpHandler) Introspect(introspectionEndpoint string, handler func(ctx context.Context, req *x_oidc.IntrospectionReq, r *http.Request) (*model.IntrospectionModel, error)) { +func (h *HttpWrapper) Introspect(introspectionEndpoint string, handler func(ctx context.Context, req *x_oidc.IntrospectionReq, r *http.Request) (*model.IntrospectionModel, error)) { h.handler.HandleFunc(introspectionEndpoint, func(w http.ResponseWriter, r *http.Request) { var introspectionReq x_oidc.IntrospectionReq r.ParseForm() @@ -207,7 +207,7 @@ func (h *HttpHandler) Introspect(introspectionEndpoint string, handler func(ctx }) } -func (h *HttpHandler) RevokeToken(revocationEndpoint string, handler func(ctx context.Context, req *x_oidc.RevokeTokenReq, r *http.Request) error) { +func (h *HttpWrapper) RevokeToken(revocationEndpoint string, handler func(ctx context.Context, req *x_oidc.RevokeTokenReq, r *http.Request) error) { h.handler.HandleFunc(revocationEndpoint, func(w http.ResponseWriter, r *http.Request) { var revokeTokenReq x_oidc.RevokeTokenReq r.ParseForm() @@ -246,7 +246,7 @@ func (h *HttpHandler) RevokeToken(revocationEndpoint string, handler func(ctx co }) } -func (h *HttpHandler) TokenExchange(tokenExchangeEndpoint string, handler func(ctx context.Context, req *x_oidc.TokenExchangeReq, r *http.Request) (interface{}, error)) { +func (h *HttpWrapper) TokenExchange(tokenExchangeEndpoint string, handler func(ctx context.Context, req *x_oidc.TokenExchangeReq, r *http.Request) (interface{}, error)) { h.handler.HandleFunc(tokenExchangeEndpoint, func(w http.ResponseWriter, r *http.Request) { var tokenExchangeReq x_oidc.TokenExchangeReq r.ParseForm() @@ -315,7 +315,7 @@ func (h *HttpHandler) TokenExchange(tokenExchangeEndpoint string, handler func(c }) } -func (h *HttpHandler) Userinfo(userinfoEndpoint string, handler func(ctx context.Context, req *x_oidc.UserinfoReq, r *http.Request) (*model.UserInfo, error)) { +func (h *HttpWrapper) Userinfo(userinfoEndpoint string, handler func(ctx context.Context, req *x_oidc.UserinfoReq, r *http.Request) (*model.UserInfo, error)) { h.handler.HandleFunc(userinfoEndpoint, func(w http.ResponseWriter, r *http.Request) { var userinfoReq x_oidc.UserinfoReq r.ParseForm() @@ -340,7 +340,7 @@ func (h *HttpHandler) Userinfo(userinfoEndpoint string, handler func(ctx context }) } -func (h *HttpHandler) AuthorizeCallback(authorizeCallbackEndpoint string, handler func(ctx context.Context, req *x_oidc.AuthorizeCallbackReq) (callbackUrl string, err error)) { +func (h *HttpWrapper) AuthorizeCallback(authorizeCallbackEndpoint string, handler func(ctx context.Context, req *x_oidc.AuthorizeCallbackReq) (callbackUrl string, err error)) { h.handler.HandleFunc(authorizeCallbackEndpoint, func(w http.ResponseWriter, r *http.Request) { var authorizeCallbackReq x_oidc.AuthorizeCallbackReq diff --git a/example/server/handler/login.go b/example/server/httpwrapper/login.go similarity index 83% rename from example/server/handler/login.go rename to example/server/httpwrapper/login.go index eb27bba..31de3be 100644 --- a/example/server/handler/login.go +++ b/example/server/httpwrapper/login.go @@ -1,4 +1,4 @@ -package handler +package httpwrapper import ( "embed" @@ -13,7 +13,7 @@ var ( templates = template.Must(template.ParseFS(templateFS, "templates/*.html")) ) -func (h *HttpHandler) login() { +func (h *HttpWrapper) login() { h.handler.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err != nil { @@ -22,8 +22,7 @@ func (h *HttpHandler) login() { } if r.Method == "GET" { templates.ExecuteTemplate(w, "login", map[string]string{ - "ID": r.Form.Get("request_id"), - "Error": "", + "ID": r.Form.Get("request_id"), }) } }) diff --git a/example/server/httpwrapper/templates/login.html b/example/server/httpwrapper/templates/login.html new file mode 100644 index 0000000..16f967e --- /dev/null +++ b/example/server/httpwrapper/templates/login.html @@ -0,0 +1,23 @@ +{{ define "login" -}} + + + + + Login + + +
+ +
+ + +
+
+ + +
+ +
+ +` +{{- end }} \ No newline at end of file diff --git a/example/server/main.go b/example/server/main.go index f4b079f..4060859 100644 --- a/example/server/main.go +++ b/example/server/main.go @@ -3,23 +3,23 @@ package main import ( "github.com/go-jose/go-jose/v3" x_oidc "github.com/xslasd/x-oidc" - "github.com/xslasd/x-oidc/crypto" - "github.com/xslasd/x-oidc/example/server/handler" + "github.com/xslasd/x-oidc/example/server/httpwrapper" "github.com/xslasd/x-oidc/example/server/storage" + "github.com/xslasd/x-oidc/util" ) func main() { - httpHandler := handler.NewHttpHandler(":8080") - cr, err := crypto.NewJoseRSAJWT("private.pem", jose.RS256) + httpHandler := httpwrapper.NewHttpHandler(":8080") + cr, err := util.NewJoseRSAJWT("private.pem", jose.RS256) if err != nil { panic(err) } _, err = x_oidc.NewOpenIDProvider( &x_oidc.Config{ - Issuer: "http://localhost:8080", - Handler: httpHandler, - Storage: storage.NewStorage(), - Crypto: cr, + Issuer: "http://localhost:8080", + OpenIDWrapper: httpHandler, + Storage: storage.NewStorage(), + Crypto: cr, }, x_oidc.WithAllowInsecure(true), ) diff --git a/model/tokenclaims.go b/model/tokenclaims.go index fa80eea..d8c5a51 100644 --- a/model/tokenclaims.go +++ b/model/tokenclaims.go @@ -79,7 +79,7 @@ func (t *TokenClaims) CheckAuthorizationContextClassReference(acr string) error type JWTClientTokenClaims struct { Issuer string `json:"iss"` Subject string `json:"sub"` - Audience []string `json:"aud"` //todo array or string + Audience Audience `json:"aud"` IssuedAt int64 `json:"iat"` ExpiresAt int64 `json:"exp"` diff --git a/op.go b/op.go index 0992513..16b2bcf 100644 --- a/op.go +++ b/op.go @@ -17,7 +17,7 @@ type OpenIDProvider struct { opt *OpenIDOption } -type OpenIDHandler interface { +type OpenIDWrapper interface { SetLogger(logger log.Logger) DiscoveryJWKs(jwksEndpoint string, handler func() (*jose.JSONWebKeySet, error)) @@ -38,7 +38,7 @@ func NewOpenIDProvider(cfg *Config, opts ...Option) (*OpenIDProvider, error) { if err != nil { return nil, err } - if cfg.Handler == nil { + if cfg.OpenIDWrapper == nil { return nil, ecode.HandlerIsNull } if cfg.Storage == nil { @@ -52,10 +52,10 @@ func NewOpenIDProvider(cfg *Config, opts ...Option) (*OpenIDProvider, error) { if !opt.allowInsecure && !util.IsHttpsPrefix(cfg.Issuer) { return nil, ecode.IssuerHTTPSInvalid } - handler := srv.cfg.Handler + handler := srv.cfg.OpenIDWrapper srv.printBanner() cfg.Storage.SetLogger(opt.logger) - cfg.Handler.SetLogger(opt.logger) + cfg.OpenIDWrapper.SetLogger(opt.logger) handler.DiscoveryJWKs(opt.jwksPath, srv.discoveryJWKs) opt.logger.Infof("JWKsEndpoint -> %s", opt.jwksEndpoint) handler.DiscoveryConfig(opt.discoveryPath, srv.discoveryConfig) diff --git a/rp/oidc_client.go b/rp/oidc_client.go index d55ecfe..f1f9dac 100644 --- a/rp/oidc_client.go +++ b/rp/oidc_client.go @@ -8,7 +8,6 @@ import ( "github.com/go-jose/go-jose/v3" "github.com/go-jose/go-jose/v3/jwt" "github.com/xslasd/x-oidc/constant" - "github.com/xslasd/x-oidc/crypto" "github.com/xslasd/x-oidc/ecode" "github.com/xslasd/x-oidc/model" "github.com/xslasd/x-oidc/util" @@ -66,7 +65,7 @@ func (o OIDCClient) GenerateCodeChallenge(CodeChallengeMethod string) string { return util.RandomString(43) case constant.CodeChallengeMethodS256: codeChallenge := util.RandomString(43) - return crypto.HashString(sha256.New(), codeChallenge, false) + return util.HashString(sha256.New(), codeChallenge, false) } return "" } diff --git a/token_exchange.go b/token_exchange.go index 8f93094..bacac5b 100644 --- a/token_exchange.go +++ b/token_exchange.go @@ -6,9 +6,9 @@ import ( "fmt" "github.com/go-jose/go-jose/v3" "github.com/xslasd/x-oidc/constant" - "github.com/xslasd/x-oidc/crypto" "github.com/xslasd/x-oidc/ecode" "github.com/xslasd/x-oidc/storage" + "github.com/xslasd/x-oidc/util" "net/http" "net/url" "strings" @@ -136,8 +136,7 @@ func authorizeCodeChallenge(req *TokenExchangeReq, authReq *storage.AuthRequest) } switch authReq.CodeChallengeMethod { case constant.CodeChallengeMethodS256: - req.CodeVerifier = crypto.HashString(sha256.New(), req.CodeVerifier, false) - default: + req.CodeVerifier = util.HashString(sha256.New(), req.CodeVerifier, false) } if req.CodeVerifier != authReq.CodeChallenge { fmt.Println("debug: CodeChallengeInvalid") diff --git a/util/issuer.go b/util/issuer.go index 38375c0..9eff427 100644 --- a/util/issuer.go +++ b/util/issuer.go @@ -24,17 +24,3 @@ func ValidateIssuer(issuer string) error { func IsHttpsPrefix(issuer string) bool { return strings.HasPrefix(issuer, constant.HttpsPrefix) } - -func GetQueryString(queryMap map[string]string) string { - if queryMap == nil || len(queryMap) == 0 { - return "" - } - queryValue := url.Values{} - for key, value := range queryMap { - if value == "" { - continue - } - queryValue.Add(key, value) - } - return queryValue.Encode() -} diff --git a/crypto/sign.go b/util/jwtcertifier.go similarity index 85% rename from crypto/sign.go rename to util/jwtcertifier.go index c24d042..0a4f972 100644 --- a/crypto/sign.go +++ b/util/jwtcertifier.go @@ -1,9 +1,12 @@ -package crypto +package util import ( "crypto/rand" "crypto/rsa" + "crypto/sha256" + "crypto/sha512" "crypto/x509" + "encoding/base64" "encoding/hex" "encoding/pem" "fmt" @@ -11,6 +14,7 @@ import ( "github.com/go-jose/go-jose/v3/jwt" "github.com/google/uuid" "github.com/xslasd/x-oidc/ecode" + "hash" "os" ) @@ -132,7 +136,6 @@ func (j JoseRSAJWT) ParseJWT(token string, payload interface{}) error { //} // //func (j JoseHMACJWT) GenerateJWT(claims interface{}) (string, error) { -// // 创建一个 JWT 签名者 // signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: j.signingKey}, nil) // if err != nil { // fmt.Println("Error creating signer:", err) @@ -161,19 +164,26 @@ func (j JoseRSAJWT) ParseJWT(token string, payload interface{}) error { // err = json.Unmarshal(b, payload) // return err //} -// -//func Sign(object interface{}, signingKey jose.SigningKey) (string, error) { -// signer, err := jose.NewSigner(signingKey, &jose.SignerOptions{}) -// if err != nil { -// return "", err -// } -// payload, err := json.Marshal(object) -// if err != nil { -// return "", err -// } -// result, err := signer.Sign(payload) -// if err != nil { -// return "", err -// } -// return result.CompactSerialize() -//} + +func GetHashAlgorithm(sigAlgorithm jose.SignatureAlgorithm) (hash.Hash, error) { + switch sigAlgorithm { + case jose.RS256, jose.ES256, jose.PS256, jose.HS256: + return sha256.New(), nil + case jose.RS384, jose.ES384, jose.PS384, jose.HS384: + return sha512.New384(), nil + case jose.RS512, jose.ES512, jose.PS512, jose.HS512: + return sha512.New(), nil + default: + return nil, ecode.AlgorithmUnsupported.SetDescriptionf(string(sigAlgorithm)) + } +} + +func HashString(hash hash.Hash, s string, firstHalf bool) string { + hash.Write([]byte(s)) + size := hash.Size() + if firstHalf { + size = size / 2 + } + sum := hash.Sum(nil)[:size] + return base64.RawURLEncoding.EncodeToString(sum) +}