diff --git a/docs/token.md b/docs/token.md index 05e7598..112025a 100644 --- a/docs/token.md +++ b/docs/token.md @@ -14,7 +14,8 @@ "sub": "id:{userID};name:{username}", "iat": 0000000000, "jti": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // 32字のランダム文字列 - "scope": "openid profile email" // スペース区切り + "scope": "openid profile email", // スペース区切り + "auth_time": 0000000000, // 認証日時 } ``` @@ -33,5 +34,6 @@ "exp": 0000000000, "iat": 0000000000, // トークン発行日時 "jti": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // 32字のランダム文字列 + "auth_time": 0000000000, // 認証日時 } ``` diff --git a/frontend/src/utils/api.d.ts b/frontend/src/utils/api.d.ts index 5189dc4..4517fe1 100644 --- a/frontend/src/utils/api.d.ts +++ b/frontend/src/utils/api.d.ts @@ -163,6 +163,23 @@ export interface paths { patch?: never trace?: never } + '/userinfo': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** Get userinfo */ + get: operations['Userinfo_getUserinfo'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } '/users/signup': { parameters: { query?: never @@ -278,6 +295,10 @@ export interface components { id: number name: string } + 'Userinfo.UserinfoRes': { + sub: string + name?: string + } 'Users.ReqSignup': { name: string password: string @@ -735,7 +756,6 @@ export interface operations { /** @description There is no content to send for this request, but the headers may be useful. */ 204: { headers: { - 'set-cookie': string [name: string]: unknown } content?: never @@ -766,13 +786,32 @@ export interface operations { /** @description There is no content to send for this request, but the headers may be useful. */ 204: { headers: { - 'set-cookie': string [name: string]: unknown } content?: never } } } + Userinfo_getUserinfo: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description The request has succeeded. */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['Userinfo.UserinfoRes'] + } + } + } + } UsersInterface_signup: { parameters: { query?: never diff --git a/internal/api/approval.go b/internal/api/approval.go index 95126fd..69efeac 100644 --- a/internal/api/approval.go +++ b/internal/api/approval.go @@ -8,7 +8,7 @@ import ( ) func (s *Server) ApprovalsInterfaceApprove(ctx context.Context, request ApprovalsInterfaceApproveRequestObject) (ApprovalsInterfaceApproveResponseObject, error) { - session, err := CurrentUser(ctx) + session, err := s.Auth.CurrentUser(ctx) if err != nil { s.logger.Errorf("failed to get current user: %v", err) return nil, err diff --git a/internal/api/client.go b/internal/api/client.go index a35ae53..741160c 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -32,7 +32,7 @@ func (s *Server) ClientsInterfaceGetClient(ctx context.Context, request ClientsI // ----------------------------------- private api ------------------------------- func (s *Server) AccountClientsCreateClient(ctx context.Context, request AccountClientsCreateClientRequestObject) (AccountClientsCreateClientResponseObject, error) { - session, err := CurrentUser(ctx) + session, err := s.Auth.CurrentUser(ctx) if err != nil { s.logger.Errorf("failed to get current user: %v", err) return nil, err @@ -55,7 +55,7 @@ func (s *Server) AccountClientsCreateClient(ctx context.Context, request Account } func (s *Server) AccountClientsDeleteClient(ctx context.Context, request AccountClientsDeleteClientRequestObject) (AccountClientsDeleteClientResponseObject, error) { - session, err := CurrentUser(ctx) + session, err := s.Auth.CurrentUser(ctx) if err != nil { s.logger.Errorf("failed to get current user: %v", err) return nil, err @@ -68,7 +68,7 @@ func (s *Server) AccountClientsDeleteClient(ctx context.Context, request Account } func (s *Server) AccountClientsListClients(ctx context.Context, request AccountClientsListClientsRequestObject) (AccountClientsListClientsResponseObject, error) { - session, err := CurrentUser(ctx) + session, err := s.Auth.CurrentUser(ctx) if err != nil { s.logger.Errorf("failed to get current user: %v", err) return nil, err @@ -92,7 +92,7 @@ func (s *Server) AccountClientsListClients(ctx context.Context, request AccountC } func (s *Server) AccountClientsGetClient(ctx context.Context, request AccountClientsGetClientRequestObject) (AccountClientsGetClientResponseObject, error) { - session, err := CurrentUser(ctx) + session, err := s.Auth.CurrentUser(ctx) if err != nil { s.logger.Errorf("failed to get current user: %v", err) return nil, err @@ -114,7 +114,7 @@ func (s *Server) AccountClientsGetClient(ctx context.Context, request AccountCli } func (s *Server) AccountClientsUpdateClient(ctx context.Context, request AccountClientsUpdateClientRequestObject) (AccountClientsUpdateClientResponseObject, error) { - session, err := CurrentUser(ctx) + session, err := s.Auth.CurrentUser(ctx) if err != nil { s.logger.Errorf("failed to get current user: %v", err) return nil, err diff --git a/internal/api/gen.go b/internal/api/gen.go index c82b838..faf1d1e 100644 --- a/internal/api/gen.go +++ b/internal/api/gen.go @@ -25,6 +25,7 @@ import ( const ( ApiKeyAuthScopes = "ApiKeyAuth.Scopes" BasicAuthScopes = "BasicAuth.Scopes" + BearerAuthScopes = "BearerAuth.Scopes" ) // Defines values for ApprovalsApproveErr. @@ -217,6 +218,12 @@ type User struct { Name string `json:"name"` } +// UserinfoUserinfoRes defines model for Userinfo.UserinfoRes. +type UserinfoUserinfoRes struct { + Name *string `json:"name,omitempty"` + Sub string `json:"sub"` +} + // UsersReqSignup defines model for Users.ReqSignup. type UsersReqSignup struct { Name string `json:"name"` @@ -320,6 +327,9 @@ type ServerInterface interface { // Login // (POST /session) SessionInterfaceLogin(ctx echo.Context) error + // Get userinfo + // (GET /userinfo) + UserinfoGetUserinfo(ctx echo.Context) error // Signup // (POST /users/signup) UsersInterfaceSignup(ctx echo.Context) error @@ -577,6 +587,17 @@ func (w *ServerInterfaceWrapper) SessionInterfaceLogin(ctx echo.Context) error { return err } +// UserinfoGetUserinfo converts echo context to params. +func (w *ServerInterfaceWrapper) UserinfoGetUserinfo(ctx echo.Context) error { + var err error + + ctx.Set(BearerAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.UserinfoGetUserinfo(ctx) + return err +} + // UsersInterfaceSignup converts echo context to params. func (w *ServerInterfaceWrapper) UsersInterfaceSignup(ctx echo.Context) error { var err error @@ -629,6 +650,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.DELETE(baseURL+"/session", wrapper.SessionInterfaceLogout) router.GET(baseURL+"/session", wrapper.SessionInterfaceMe) router.POST(baseURL+"/session", wrapper.SessionInterfaceLogin) + router.GET(baseURL+"/userinfo", wrapper.UserinfoGetUserinfo) router.POST(baseURL+"/users/signup", wrapper.UsersInterfaceSignup) } @@ -987,16 +1009,10 @@ type SessionInterfaceLogoutResponseObject interface { VisitSessionInterfaceLogoutResponse(w http.ResponseWriter) error } -type SessionInterfaceLogout204ResponseHeaders struct { - SetCookie string -} - type SessionInterfaceLogout204Response struct { - Headers SessionInterfaceLogout204ResponseHeaders } func (response SessionInterfaceLogout204Response) VisitSessionInterfaceLogoutResponse(w http.ResponseWriter) error { - w.Header().Set("set-cookie", fmt.Sprint(response.Headers.SetCookie)) w.WriteHeader(204) return nil } @@ -1025,16 +1041,10 @@ type SessionInterfaceLoginResponseObject interface { VisitSessionInterfaceLoginResponse(w http.ResponseWriter) error } -type SessionInterfaceLogin204ResponseHeaders struct { - SetCookie string -} - type SessionInterfaceLogin204Response struct { - Headers SessionInterfaceLogin204ResponseHeaders } func (response SessionInterfaceLogin204Response) VisitSessionInterfaceLoginResponse(w http.ResponseWriter) error { - w.Header().Set("set-cookie", fmt.Sprint(response.Headers.SetCookie)) w.WriteHeader(204) return nil } @@ -1051,6 +1061,22 @@ func (response SessionInterfaceLogin400JSONResponse) VisitSessionInterfaceLoginR return json.NewEncoder(w).Encode(response) } +type UserinfoGetUserinfoRequestObject struct { +} + +type UserinfoGetUserinfoResponseObject interface { + VisitUserinfoGetUserinfoResponse(w http.ResponseWriter) error +} + +type UserinfoGetUserinfo200JSONResponse UserinfoUserinfoRes + +func (response UserinfoGetUserinfo200JSONResponse) VisitUserinfoGetUserinfoResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + type UsersInterfaceSignupRequestObject struct { Body *UsersInterfaceSignupJSONRequestBody } @@ -1132,6 +1158,9 @@ type StrictServerInterface interface { // Login // (POST /session) SessionInterfaceLogin(ctx context.Context, request SessionInterfaceLoginRequestObject) (SessionInterfaceLoginResponseObject, error) + // Get userinfo + // (GET /userinfo) + UserinfoGetUserinfo(ctx context.Context, request UserinfoGetUserinfoRequestObject) (UserinfoGetUserinfoResponseObject, error) // Signup // (POST /users/signup) UsersInterfaceSignup(ctx context.Context, request UsersInterfaceSignupRequestObject) (UsersInterfaceSignupResponseObject, error) @@ -1546,6 +1575,29 @@ func (sh *strictHandler) SessionInterfaceLogin(ctx echo.Context) error { return nil } +// UserinfoGetUserinfo operation middleware +func (sh *strictHandler) UserinfoGetUserinfo(ctx echo.Context) error { + var request UserinfoGetUserinfoRequestObject + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.UserinfoGetUserinfo(ctx.Request().Context(), request.(UserinfoGetUserinfoRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UserinfoGetUserinfo") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(UserinfoGetUserinfoResponseObject); ok { + return validResponse.VisitUserinfoGetUserinfoResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} + // UsersInterfaceSignup operation middleware func (sh *strictHandler) UsersInterfaceSignup(ctx echo.Context) error { var request UsersInterfaceSignupRequestObject @@ -1578,40 +1630,41 @@ func (sh *strictHandler) UsersInterfaceSignup(ctx echo.Context) error { // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "R3e/4tXN6u8AAAD//xkvUt4LNgAA", + "H4sIAAAAAAAC/+xaW2/bOBb+KwR3gXlxbE8bLLB+S7OLTGem2yBpsQ9FINDkscVWJhVekngK//cFSUmW", + "ZMqWE7tJu31JZIk8PJfvXEier5jKRS4FCKPx5CvWNIUF8Y9nlEorzHnG3cfhuQJiIPy6gls3IlcyB2U4", + "+PGCLMD9N8sc8ARro7iY49UAK2BcATWJVZkfyQ0sdHRo8YIoRZZ45efeWq6A4cmnsECb3E01SU4/AzWO", + "SpRzFn5u8s1ZlJXDizPAGqgCExnakpQzPCjFLeY8Qu6POftuLJbnSt6RTA/DE/xbKbccCLvwChF3JOMs", + "ocGGg+qFpjKHGsk1YxskoyoIBJMODATqO821JlJOicn47PjbArLdFipBdQEFvloWKnQgpElm0goWtckG", + "kauggphN3NPfFczwBP9ttA5SoyJCjS7tNOO0UGrcIlFB3p9Zkw7dH6n4X11Ac8RAO6RZQcqxNfwRSkHr", + "hIHgwPwobfNcKgNurs6l0JD4tdtYdR6t7kAloJRUeIANLHKpiOLZMrGC3BGekWkWB3WL+Su4fWczwy+J", + "Mvtie0EeEjL3EJtJtSAGTzAX5vUrXK3LhYE5qBboeAcq60LvsN2GGGHuBzd1i9sNsDbE9HDItgXqDtqQ", + "ZJu/bmOyDnzJttnqg/wC4pzQFM6lMEpmvwFh0MCckCfaSLWTyg6o7ph9oYgwbeZLaBPDpUj6iHKpyHxB", + "Hi/EI6KwZyv2Ye5E2gNwLT3shnULVrX1Cq424LSWpBtShR4ika+IKsYNiEoMDzlXoBMuevotZzVq3GTu", + "81uG/LsB4jPk8Y+4oJlloNEvMgfB2S84YkU/aV91hz9e3S1tNoRtUG/IuUOPa/o1ML4BokBFodhIG0/M", + "xZ0JNcbzNWjNpRj+Kee8y5Xd5ESqJCda30sVT6INQvvVdRXdnbIUdUGEkahE0rbrAVcIZHI+B9Y04qYc", + "7yDqC1aHAEOy7P0MTz5tR9xHN3p1M8DCZiF9ToyysMF1S0y/SEy2j8XqMXjU3e4fp1G3OwRgHAtczOSw", + "fIiqqdPW2k53c+AGda2th1dwe83nwuaHgdj6Y0KlmHGnRS7F/mDsItQtSRCjDVHnbRmIuUl93QpC2nla", + "mCQhmQLClonV0FgxNiHKjh+yIIamEfiHzaBV3CyvHYSDVs9y/gcsXXDzaBN4gqmUXziUOJn4jL2GHPET", + "nJxviOa0nOm9wn2furfr4akxuR/s4+Pm6BA2W8Mdrw5+9RRyFri4A6W9BfF4OB6OHWmXP0jO8QS/9q+c", + "ckzqpRuRsD8dkXJr5nEltQ/EDl1eb2+ZW6Ac8lYYUDNCodjF4QAN0OaNZEtfPEhhimBO8jzj1FMZfdYB", + "WiFC7MpY0d3iqglEF1Bqla7n/tX41P1joKnieYAz/pCCAsQ1EhIV3CEjkQbB0EwqZFKuUSHFAE2tQSYF", + "lPqaSqMFWaIpIKthZrMhcko9HY/3krTpq2Gjsa8CnLO4isNNThry7XLXcmOzOXXTQd3cDeWhsENCVNqM", + "ISENssKpxhDBvKoK3SFmwSm2SJ9IL4UhD8OGd/nMUferTzcuTWi7WBC1rJAGiCDnWiVpT6MCbKjqvDLn", + "EENr4+DlT67LR7wBl6cYssZGteffZtNyc7zjIKAk29c6pfZTopG2lAIwYHtq/QIMIlmGyrVdcogHgoZq", + "64eQT4gEjzlv2HEc2vsEok9M+fV5pGH7nqbsgQ/kfJcgAfdIgZZWUfADpgAC0bA8IhoR99lmZnjAuNcn", + "XL3E0BSsUmiNrm3Tjkujr5ytQhrKIJyPbHOif/lRlRPlRJEFGFDaM+SrDpez1zVHcW5Sx+ygpvW2em++", + "5xz5vWIlGBWRCieDPqmqOoj9NjgYHz2uHTGCFRHp9KWijAgHsRlvggtYFW8fk6JrcOqTn+tXTkeD1LPl", + "/PaF2kFz/s8YefQYGexXA7XLpe0cGo2aBQKqzeiLjpu742PrBu4RUfDo+9HofeP3vB/tiKsOgimQzKR/", + "daLvt/D9PAX6pWNHud8ubV1hOpIorB94kW4XPKquPDt58kfw68OZavyGP8BDnvkLnBnJNAyCf9xaUMu1", + "g7Tv67p95dG3iqtBP1aaV4V9XXbQV87GZdHB6Zc3zIcn7O9dD0CovHWuk9p5jfVMO4rX41ebq1wVJnS/", + "BriY7YZlklbH2f31v/o20TTS9NAzlva+c39hUfasfrONrspTva4ythnOLqU29ZDWXXMubGZ4TpQZORSf", + "MGJI/2S8rZvjxdSMP73gx/OCdaL/fP9F98zxF2B+d6OfWJ0Sxrj7RLLLugVXBzh8bpRYv1+//w/6L0zR", + "H7BE19CQumqJ6BMMLsB8KHoUYqV+AH/zgq5S+XE2tw8n9/f3Jz7gWJWBoJIB2zfsVN04vSLN4XYgrR6Y", + "/czciDWU0BROaGir2h5wejIU6dNy7OW+5+kgKzTap75x4KuayH6Ua73anXfs8Cp4uXd7HXpNtp2RF+0o", + "lduHrhb8DDl3nyONgsuu0962UO8AH9G1mx09R749LE3aWdJFDMrFkdoHNnqy/p9aBzY6236IY5oAFx89", + "bNGA1VkolR1aF2DKx2P6WbQz7InuVm8JirlbpYNKI3qk181hUQ/0vVeV/xWtZMdxwHbD2svwv0a5osGc", + "FN1cL3Fz1G6U+yGcuI6GQDhU7s1lHerRtf+MB9iqrOh905ORPwkd5lws/3k6HlK5GJGcj+5+xaub1f8C", + "AAD//9CWBlY8NwAA", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/api/init.go b/internal/api/init.go index 9d888f0..3ffbe57 100644 --- a/internal/api/init.go +++ b/internal/api/init.go @@ -3,12 +3,8 @@ package api import ( "auth/internal/domain" "encoding/gob" - "os" - - "github.com/gorilla/sessions" ) func Init() { gob.Register(&domain.User{}) - store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET"))) } diff --git a/internal/api/middleware/auth.go b/internal/api/middleware/auth.go new file mode 100644 index 0000000..6e029d6 --- /dev/null +++ b/internal/api/middleware/auth.go @@ -0,0 +1,264 @@ +package middleware + +import ( + "auth/internal/api" + "auth/internal/domain" + "auth/internal/domain/oauth" + "context" + "crypto/rsa" + "encoding/base64" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/getkin/kin-openapi/openapi3filter" + "github.com/golang-jwt/jwt" + "github.com/gorilla/sessions" + "github.com/labstack/echo/v4" + middleware "github.com/oapi-codegen/echo-middleware" +) + +type AuthMiddleware struct { + clientRepo oauth.IClientRepo + userRepo domain.IUserRepo + issuer string + rsaPubKey *rsa.PublicKey + sessionStore *sessions.CookieStore +} + +const sessionName = "auth.middleware" +const sessionUserKey = "user" +const sessionAuthTimeKey = "auth_time" + +type typeAuthContextKey string + +const userContextKey typeAuthContextKey = "current_user" +const scopesContextKey typeAuthContextKey = "scopes" + +func NewAuthMiddleware(conf *Config, clientRepo oauth.IClientRepo, userRepo domain.IUserRepo) *AuthMiddleware { + pubKey, err := jwt.ParseRSAPublicKeyFromPEM([]byte(conf.RsaPublicKey)) + if err != nil { + panic(err) + } + store := sessions.NewCookieStore([]byte(conf.SessionSecret)) + return &AuthMiddleware{ + clientRepo: clientRepo, + userRepo: userRepo, + issuer: conf.Issuer, + rsaPubKey: pubKey, + sessionStore: store, + } +} + +func (m *AuthMiddleware) Auth() echo.MiddlewareFunc { + spec, err := api.GetSwagger() + if err != nil { + panic(err) + } + spec.Servers = nil // HACK: https://github.com/deepmap/oapi-codegen/blob/master/examples/petstore-expanded/echo/petstore.go#L30-L32 + validator := middleware.OapiRequestValidatorWithOptions(spec, + &middleware.Options{ + Options: openapi3filter.Options{ + AuthenticationFunc: m.authenticate, + }, + }) + return validator +} + +var ErrUnsupportedAuthenticationScheme = fmt.Errorf("unsupported authentication scheme") + +func (m *AuthMiddleware) authenticate(ctx context.Context, input *openapi3filter.AuthenticationInput) error { + if input.SecuritySchemeName == "BasicAuth" { + return m.authClient(ctx, input) + } + if input.SecuritySchemeName == "BearerAuth" { + return m.bearerAuth(ctx, input) + } + if input.SecuritySchemeName != "ApiKeyAuth" || input.SecurityScheme.In != "cookie" { + return ErrUnsupportedAuthenticationScheme + } + return m.cookieAuth(ctx, input) +} + +func (m *AuthMiddleware) bearerAuth(_ context.Context, input *openapi3filter.AuthenticationInput) error { + req := input.RequestValidationInput.Request + auth := req.Header.Get("Authorization") + if !strings.HasPrefix(auth, "Bearer ") { + return fmt.Errorf("invalid bearer token") + } + token := auth[7:] + tok, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + return m.rsaPubKey, nil + }) + if err != nil { + return fmt.Errorf("invalid token: %w", err) + } + claims := tok.Claims.(jwt.MapClaims) + if claims["iss"] != m.issuer { + return fmt.Errorf("invalid issuer") + } + if int64(claims["exp"].(float64)) < time.Now().Unix() { + return fmt.Errorf("token expired") + } + sub := claims["sub"].(string) + idStr := strings.Split(strings.Split(sub, ";")[0], ":")[1] + userID, err := strconv.Atoi(idStr) + if err != nil { + return fmt.Errorf("invalid user id") + } + user, err := m.userRepo.FindByID(domain.UserID(userID)) + if err != nil { + return fmt.Errorf("user not found: %w", err) + } + authTime := int64(claims["auth_time"].(float64)) + authSession := &api.AuthSession{ + User: user, + AuthTime: time.Unix(authTime, 0), + } + scopesStr := strings.Split(claims["scope"].(string), " ") + scopes := make([]oauth.TypeScope, 0, len(scopesStr)) + for _, s := range scopesStr { + scopes = append(scopes, oauth.TypeScope(s)) + } + newCtx := context.WithValue(req.Context(), userContextKey, authSession) + newCtx = context.WithValue(newCtx, scopesContextKey, scopes) + newReq := req.WithContext(newCtx) + *req = *newReq + return err +} + +func (m *AuthMiddleware) cookieAuth(_ context.Context, input *openapi3filter.AuthenticationInput) error { + req := input.RequestValidationInput.Request + authSession, err := authSessionFromCookie(req, m.sessionStore) + if err != nil { + return fmt.Errorf("failed to get auth session from cookie: %w", err) + } + newReq := req.WithContext(context.WithValue(req.Context(), userContextKey, authSession)) + *req = *newReq + return nil +} + +func (m *AuthMiddleware) authClient(_ context.Context, input *openapi3filter.AuthenticationInput) error { + auth := input.RequestValidationInput.Request.Header.Get("Authorization") + if !strings.HasPrefix(auth, "Basic ") { + return fmt.Errorf("invalid basic auth header") + } + decoded, err := base64.StdEncoding.DecodeString(auth[6:]) + if err != nil { + return err + } + clientArr := strings.Split(string(decoded), ":") + if len(clientArr) != 2 { + return fmt.Errorf("invalid basic auth header") + } + client, err := m.clientRepo.FindByID(oauth.ClientID(clientArr[0])) + if err != nil { + return err + } + return client.SecretCorrect(clientArr[1]) +} + +type Auth struct { + echoContextReg *EchoContextReg + store *sessions.CookieStore +} + +var _ api.Auth = &Auth{} + +func NewAuth(conf *Config, echoContextReg *EchoContextReg) api.Auth { + return &Auth{ + echoContextReg: echoContextReg, + store: sessions.NewCookieStore([]byte(conf.SessionSecret)), + } +} + +func (a *Auth) CurrentUser(ctx context.Context) (*api.AuthSession, error) { + authSession, ok := ctx.Value(userContextKey).(*api.AuthSession) + if ok { + return authSession, nil + } + echoCtx, err := a.echoContextReg.Context(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get echo context: %w", err) + } + req := echoCtx.Request() + authSession, err = authSessionFromCookie(req, a.store) + if err != nil { + return nil, fmt.Errorf("failed to get auth session from cookie: %w", err) + } + return authSession, nil +} + +func (a *Auth) AccessScopes(ctx context.Context) ([]oauth.TypeScope, error) { + scopes, ok := ctx.Value(scopesContextKey).([]oauth.TypeScope) + if !ok { + return nil, api.ErrUnauthorized + } + return scopes, nil +} + +func (a *Auth) Login(ctx context.Context, user *domain.User) error { + echoCtx, err := a.echoContextReg.Context(ctx) + if err != nil { + return fmt.Errorf("failed to get echo context: %w", err) + } + reg := sessions.GetRegistry(echoCtx.Request()) + session, err := reg.Get(a.store, sessionName) + if err != nil { + return fmt.Errorf("failed to get session: %w", err) + } + session.Values[sessionUserKey] = user + session.Values[sessionAuthTimeKey] = time.Now().Unix() + if err := session.Save(echoCtx.Request(), echoCtx.Response().Writer); err != nil { + return fmt.Errorf("failed to save session: %w", err) + } + return nil +} + +func (a *Auth) Logout(ctx context.Context) error { + echoCtx, err := a.echoContextReg.Context(ctx) + if err != nil { + return fmt.Errorf("failed to get echo context: %w", err) + } + reg := sessions.GetRegistry(echoCtx.Request()) + session, err := reg.Get(a.store, sessionName) + if err != nil { + return fmt.Errorf("failed to get session: %w", err) + } + delete(session.Values, sessionUserKey) + delete(session.Values, sessionAuthTimeKey) + if err := session.Save(echoCtx.Request(), echoCtx.Response().Writer); err != nil { + return fmt.Errorf("failed to save session: %w", err) + } + return nil +} + +func authSessionFromCookie(req *http.Request, store *sessions.CookieStore) (*api.AuthSession, error) { + reg := sessions.GetRegistry(req) + session, err := reg.Get(store, sessionName) + if err != nil { + return nil, fmt.Errorf("failed to get session: %w", err) + } + userObj, ok := session.Values[sessionUserKey] + if !ok { + return nil, api.ErrUnauthorized + } + user, ok := userObj.(*domain.User) + if !ok { + return nil, fmt.Errorf("failed to get user from session") + } + authTime, ok := session.Values[sessionAuthTimeKey] + if !ok { + return nil, api.ErrUnauthorized + } + t, ok := authTime.(int64) + if !ok { + return nil, fmt.Errorf("failed to get auth time from session") + } + return &api.AuthSession{ + User: user, + AuthTime: time.Unix(t, 0), + }, nil +} diff --git a/internal/api/middleware/config.go b/internal/api/middleware/config.go new file mode 100644 index 0000000..72b9abd --- /dev/null +++ b/internal/api/middleware/config.go @@ -0,0 +1,18 @@ +package middleware + +import "github.com/kelseyhightower/envconfig" + +type Config struct { + Issuer string `required:"true" envconfig:"OAUTH_ISSUER"` + RsaPublicKey string `required:"true" envconfig:"OAUTH_RSA_PUBLIC_KEY"` + SessionSecret string `required:"true" envconfig:"SESSION_SECRET"` +} + +func NewConfig() *Config { + conf := &Config{} + err := envconfig.Process("middleware", conf) + if err != nil { + panic(err) + } + return conf +} diff --git a/internal/api/middleware/context.go b/internal/api/middleware/context.go new file mode 100644 index 0000000..d2ae001 --- /dev/null +++ b/internal/api/middleware/context.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "auth/internal/api" + "context" + "fmt" + + "github.com/labstack/echo/v4" +) + +type EchoContextMiddleware struct{} + +func NewEchoContextMiddleware() *EchoContextMiddleware { + return &EchoContextMiddleware{} +} + +type typeEchoContextKey string + +const echoContextKey typeEchoContextKey = "echoContext" + +func (m *EchoContextMiddleware) Context() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + req := c.Request() + newReq := req.WithContext(context.WithValue(req.Context(), echoContextKey, c)) + + *req = *newReq + return next(c) + } + } +} + +type EchoContextReg struct{} + +var _ api.EchoContextReg = &EchoContextReg{} + +func NewEchoContextReg() *EchoContextReg { + return &EchoContextReg{} +} + +func (r *EchoContextReg) Context(c context.Context) (echo.Context, error) { + echoContext, ok := c.Value(echoContextKey).(echo.Context) + if !ok { + return nil, fmt.Errorf("failed to get echo.Context from context") + } + return echoContext, nil +} diff --git a/internal/api/oauth.go b/internal/api/oauth.go index a9d927e..53634d4 100644 --- a/internal/api/oauth.go +++ b/internal/api/oauth.go @@ -50,7 +50,7 @@ func (s *Server) OAuthInterfaceAuthorize(ctx context.Context, request OAuthInter return s.Conf.LoginUrl + "?" + toQueryString(query), nil } - authSession, err := CurrentUser(ctx) + authSession, err := s.Auth.CurrentUser(ctx) if errors.Is(err, ErrUnauthorized) { loginUrl, err := loginUrl() if err != nil { diff --git a/internal/api/server.go b/internal/api/server.go index 9da8c1d..035d3f7 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -13,6 +13,7 @@ type Server struct { OAuthUsecase *usecase.OAuthUsecase ClientUsecase usecase.IClientUsecase Conf *Config + Auth Auth logger echo.Logger } @@ -24,7 +25,12 @@ type Config struct { var _ StrictServerInterface = &Server{} -func NewServer(userUsecase *usecase.UserUsecase, oauthUsecase *usecase.OAuthUsecase, clientUC usecase.IClientUsecase) *Server { +func NewServer( + userUsecase *usecase.UserUsecase, + oauthUsecase *usecase.OAuthUsecase, + clientUC usecase.IClientUsecase, + auth Auth, +) *Server { conf := &Config{} err := envconfig.Process("api", conf) if err != nil { @@ -35,6 +41,7 @@ func NewServer(userUsecase *usecase.UserUsecase, oauthUsecase *usecase.OAuthUsec OAuthUsecase: oauthUsecase, ClientUsecase: clientUC, Conf: conf, + Auth: auth, } } diff --git a/internal/api/session.go b/internal/api/session.go index 26c6474..69aa91f 100644 --- a/internal/api/session.go +++ b/internal/api/session.go @@ -17,21 +17,16 @@ func (s *Server) SessionInterfaceLogin(ctx context.Context, request SessionInter ErrorDescription: "name or password is incorrect", }, nil } - cookie, err := Login(ctx, user) - if err != nil { + if err := s.Auth.Login(ctx, user); err != nil { s.logger.Errorf("failed to login: %v", err) return nil, err } - return &SessionInterfaceLogin204Response{ - Headers: SessionInterfaceLogin204ResponseHeaders{ - SetCookie: cookie.String(), - }, - }, nil + return &SessionInterfaceLogin204Response{}, nil } // SessionInterfaceLogout implements StrictServerInterface. func (s *Server) SessionInterfaceLogout(ctx context.Context, request SessionInterfaceLogoutRequestObject) (SessionInterfaceLogoutResponseObject, error) { - session, err := CurrentUser(ctx) + session, err := s.Auth.CurrentUser(ctx) if err != nil { s.logger.Errorf("failed to get current user: %v", err) return nil, err @@ -39,21 +34,16 @@ func (s *Server) SessionInterfaceLogout(ctx context.Context, request SessionInte if session == nil { return nil, echo.ErrUnauthorized } - cookie, err := Logout(ctx) - if err != nil { + if err := s.Auth.Logout(ctx); err != nil { s.logger.Errorf("failed to logout: %v", err) return nil, err } - return &SessionInterfaceLogout204Response{ - Headers: SessionInterfaceLogout204ResponseHeaders{ - SetCookie: cookie.String(), - }, - }, nil + return &SessionInterfaceLogout204Response{}, nil } // SessionInterfaceMe implements StrictServerInterface. func (s *Server) SessionInterfaceMe(ctx context.Context, request SessionInterfaceMeRequestObject) (SessionInterfaceMeResponseObject, error) { - session, err := CurrentUser(ctx) + session, err := s.Auth.CurrentUser(ctx) if err != nil { s.logger.Errorf("failed to get current user: %v", err) return nil, err diff --git a/internal/api/user.go b/internal/api/user.go index e3cd9c1..6bf6c1e 100644 --- a/internal/api/user.go +++ b/internal/api/user.go @@ -42,14 +42,9 @@ func (s *Server) UsersInterfaceSignup(ctx context.Context, request UsersInterfac s.logger.Errorf("failed to signup: %v", err) return nil, err } - cookie, err := Login(ctx, user) - if err != nil { + if err := s.Auth.Login(ctx, user); err != nil { s.logger.Errorf("failed to login: %v", err) return nil, err } - return &UsersInterfaceSignup204Response{ - Headers: UsersInterfaceSignup204ResponseHeaders{ - SetCookie: cookie.String(), - }, - }, nil + return &UsersInterfaceSignup204Response{}, nil } diff --git a/internal/api/userinfo.go b/internal/api/userinfo.go new file mode 100644 index 0000000..b7d80c8 --- /dev/null +++ b/internal/api/userinfo.go @@ -0,0 +1,29 @@ +package api + +import ( + "auth/internal/domain/oauth" + "context" + "fmt" + "slices" +) + +func (s *Server) UserinfoGetUserinfo(ctx context.Context, request UserinfoGetUserinfoRequestObject) (UserinfoGetUserinfoResponseObject, error) { + scopes, err := s.Auth.AccessScopes(ctx) + fmt.Println(scopes) + if err != nil { + s.logger.Errorf("failed to get access scopes: %v", err) + return nil, err + } + session, err := s.Auth.CurrentUser(ctx) + if err != nil { + s.logger.Errorf("failed to get current user: %v", err) + return nil, err + } + res := UserinfoGetUserinfo200JSONResponse{ + Sub: fmt.Sprintf("id:%d;name:%s", session.User.ID, session.User.Name), + } + if slices.Contains(scopes, oauth.ScopeProfile) { + res.Name = ptr(session.User.Name) + } + return res, nil +} diff --git a/internal/api/util.go b/internal/api/util.go index c3ecd53..0bceb56 100644 --- a/internal/api/util.go +++ b/internal/api/util.go @@ -2,33 +2,14 @@ package api import ( "auth/internal/domain" + "auth/internal/domain/oauth" "context" "errors" - "fmt" - "net/http" "net/url" "strings" "time" - "github.com/gorilla/securecookie" - "github.com/gorilla/sessions" -) - -const SESSION_NAME = "auth" - -var sessionsOptions = &sessions.Options{ - HttpOnly: true, - SameSite: http.SameSiteLaxMode, - // Secure: true, - MaxAge: 60 * 60 * 24 * 1, -} - -var store *sessions.CookieStore - -type CtxKey string - -const ( - CtxKeySessionRegistry CtxKey = "sessionRegistry" + "github.com/labstack/echo/v4" ) type AuthSession struct { @@ -36,98 +17,15 @@ type AuthSession struct { AuthTime time.Time } -const SESSION_USER_KEY = "user" -const SESSION_AUTH_TIME_KEY = "auth_time" - -func SetSessionRegistry(r *http.Request) error { - reg := sessions.GetRegistry(r) - ctx := context.WithValue(r.Context(), CtxKeySessionRegistry, reg) - *r = *r.WithContext(ctx) - return nil -} - -func GetFromSession(c context.Context, key string) (interface{}, error) { - reg := c.Value(CtxKeySessionRegistry).(*sessions.Registry) - session, err := reg.Get(store, SESSION_NAME) - if err != nil { - return nil, err - } - v, ok := session.Values[key] - if !ok { - return nil, ErrNotFoundInSession - } - return v, nil -} -func SetToSession(c context.Context, key string, value interface{}) error { - reg := c.Value(CtxKeySessionRegistry).(*sessions.Registry) - session, err := reg.Get(store, SESSION_NAME) - if err != nil { - return err - } - session.Options = sessionsOptions - session.Values[key] = value - return nil -} -func Save(c context.Context) (*http.Cookie, error) { - reg := c.Value(CtxKeySessionRegistry).(*sessions.Registry) - session, err := reg.Get(store, SESSION_NAME) - if err != nil { - return nil, err - } - encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, store.Codecs...) - if err != nil { - return nil, err - } - return sessions.NewCookie(session.Name(), encoded, sessionsOptions), nil -} - -func CurrentUser(c context.Context) (*AuthSession, error) { - userObj, err := GetFromSession(c, SESSION_USER_KEY) - if errors.Is(err, ErrNotFoundInSession) { - return nil, ErrUnauthorized - } - if err != nil { - return nil, err - } - user, ok := userObj.(*domain.User) - if !ok { - return nil, ErrUnauthorized - } - authTime, err := GetFromSession(c, SESSION_AUTH_TIME_KEY) - if errors.Is(err, ErrNotFoundInSession) { - return nil, ErrUnauthorized - } - if err != nil { - return nil, err - } - if authTime == nil { - return nil, ErrUnauthorized - } - t, ok := authTime.(int64) - if !ok { - return nil, ErrUnauthorized - } - return &AuthSession{User: user, AuthTime: time.Unix(t, 0)}, nil +type Auth interface { + CurrentUser(ctx context.Context) (*AuthSession, error) + Login(ctx context.Context, user *domain.User) error + Logout(ctx context.Context) error + AccessScopes(ctx context.Context) ([]oauth.TypeScope, error) } -func Login(c context.Context, user *domain.User) (*http.Cookie, error) { - if err := SetToSession(c, SESSION_USER_KEY, user); err != nil { - return nil, fmt.Errorf("failed to set user session: %w", err) - } - if err := SetToSession(c, SESSION_AUTH_TIME_KEY, time.Now().Unix()); err != nil { - return nil, fmt.Errorf("failed to set auth time session: %w", err) - } - return Save(c) -} - -func Logout(c context.Context) (*http.Cookie, error) { - if err := SetToSession(c, SESSION_USER_KEY, nil); err != nil { - return nil, fmt.Errorf("failed to set user session: %w", err) - } - if err := SetToSession(c, SESSION_AUTH_TIME_KEY, nil); err != nil { - return nil, fmt.Errorf("failed to set auth time session: %w", err) - } - return Save(c) +type EchoContextReg interface { + Context(c context.Context) (echo.Context, error) } func toQueryString(h map[string]string) string { @@ -142,3 +40,7 @@ var ( ErrUnauthorized = errors.New("unauthorized") ErrNotFoundInSession = errors.New("not found in session") ) + +func ptr[T any](v T) *T { + return &v +} diff --git a/internal/di/wire.go b/internal/di/wire.go index d984b6e..01cc962 100644 --- a/internal/di/wire.go +++ b/internal/di/wire.go @@ -4,11 +4,11 @@ package di import ( "auth/internal/api" + "auth/internal/api/middleware" "auth/internal/domain" "auth/internal/domain/oauth" "auth/internal/infrastructure" "auth/internal/infrastructure/gateway" - "auth/internal/middleware" "auth/internal/usecase" "github.com/google/wire" @@ -17,6 +17,9 @@ import ( func NewServer() *api.Server { wire.Build( api.NewServer, + middleware.NewAuth, + middleware.NewConfig, + middleware.NewEchoContextReg, usecase.NewAuthUsecase, usecase.NewOAuthUsecase, gateway.NewApprovalRepo, @@ -37,9 +40,28 @@ func NewServer() *api.Server { func NewAuthMiddleware() *middleware.AuthMiddleware { wire.Build( + middleware.NewConfig, middleware.NewAuthMiddleware, + gateway.NewUserRepo, gateway.NewClientRepo, infrastructure.GetDB, ) return nil } + +func NewEchoContextMiddleware() *middleware.EchoContextMiddleware { + wire.Build( + middleware.NewEchoContextMiddleware, + ) + return nil +} + +func NewTokenService() *oauth.TokenService { + wire.Build( + oauth.NewTokenService, + oauth.NewConfig, + gateway.NewUserRepo, + infrastructure.GetDB, + ) + return nil +} diff --git a/internal/di/wire_gen.go b/internal/di/wire_gen.go index 4af2aaa..8d38271 100644 --- a/internal/di/wire_gen.go +++ b/internal/di/wire_gen.go @@ -8,11 +8,11 @@ package di import ( "auth/internal/api" + "auth/internal/api/middleware" "auth/internal/domain" "auth/internal/domain/oauth" "auth/internal/infrastructure" "auth/internal/infrastructure/gateway" - "auth/internal/middleware" "auth/internal/usecase" ) @@ -33,13 +33,31 @@ func NewServer() *api.Server { iClientRepo := gateway.NewClientRepo(db) oAuthUsecase := usecase.NewOAuthUsecase(authCodeService, jwKsService, approvalService, iApprovalRepo, tokenService, iClientRepo) iClientUsecase := usecase.NewClientUsecase(iClientRepo) - server := api.NewServer(userUsecase, oAuthUsecase, iClientUsecase) + middlewareConfig := middleware.NewConfig() + echoContextReg := middleware.NewEchoContextReg() + auth := middleware.NewAuth(middlewareConfig, echoContextReg) + server := api.NewServer(userUsecase, oAuthUsecase, iClientUsecase, auth) return server } func NewAuthMiddleware() *middleware.AuthMiddleware { + config := middleware.NewConfig() db := infrastructure.GetDB() iClientRepo := gateway.NewClientRepo(db) - authMiddleware := middleware.NewAuthMiddleware(iClientRepo) + iUserRepo := gateway.NewUserRepo(db) + authMiddleware := middleware.NewAuthMiddleware(config, iClientRepo, iUserRepo) return authMiddleware } + +func NewEchoContextMiddleware() *middleware.EchoContextMiddleware { + echoContextMiddleware := middleware.NewEchoContextMiddleware() + return echoContextMiddleware +} + +func NewTokenService() *oauth.TokenService { + config := oauth.NewConfig() + db := infrastructure.GetDB() + iUserRepo := gateway.NewUserRepo(db) + tokenService := oauth.NewTokenService(config, iUserRepo) + return tokenService +} diff --git a/internal/domain/oauth/scope.go b/internal/domain/oauth/scope.go index b010025..2b3289b 100644 --- a/internal/domain/oauth/scope.go +++ b/internal/domain/oauth/scope.go @@ -8,13 +8,15 @@ import ( type TypeScope string const ( - ScopeOpenID TypeScope = "openid" - ScopeEmail TypeScope = "email" + ScopeOpenID TypeScope = "openid" + ScopeEmail TypeScope = "email" + ScopeProfile TypeScope = "profile" ) var AllScopes = []TypeScope{ ScopeOpenID, ScopeEmail, + ScopeProfile, } func ValidScopes(scopes []TypeScope) error { diff --git a/internal/infrastructure/gateway/scope.go b/internal/infrastructure/gateway/scope.go index 29e8ae4..c304db5 100644 --- a/internal/infrastructure/gateway/scope.go +++ b/internal/infrastructure/gateway/scope.go @@ -5,8 +5,10 @@ import "auth/internal/domain/oauth" var ScopeMap = map[int32]oauth.TypeScope{ 0: oauth.ScopeOpenID, 1: oauth.ScopeEmail, + 2: oauth.ScopeProfile, } var ScopeMapReverse = map[oauth.TypeScope]int32{ - oauth.ScopeOpenID: 0, - oauth.ScopeEmail: 1, + oauth.ScopeOpenID: 0, + oauth.ScopeEmail: 1, + oauth.ScopeProfile: 2, } diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go deleted file mode 100644 index e13ea51..0000000 --- a/internal/middleware/auth.go +++ /dev/null @@ -1,78 +0,0 @@ -package middleware - -import ( - "auth/internal/api" - "auth/internal/domain/oauth" - "context" - "encoding/base64" - "fmt" - "strings" - - "github.com/getkin/kin-openapi/openapi3filter" - "github.com/labstack/echo/v4" - middleware "github.com/oapi-codegen/echo-middleware" -) - -type AuthMiddleware struct { - clientRepo oauth.IClientRepo -} - -func NewAuthMiddleware(clientRepo oauth.IClientRepo) *AuthMiddleware { - return &AuthMiddleware{ - clientRepo: clientRepo, - } -} - -// IMPORTANT: This middleware is dependent on the session middleware. -// Make sure to add the session middleware before this middleware. -func (m *AuthMiddleware) Auth() echo.MiddlewareFunc { - spec, err := api.GetSwagger() - if err != nil { - panic(err) - } - spec.Servers = nil // HACK: https://github.com/deepmap/oapi-codegen/blob/master/examples/petstore-expanded/echo/petstore.go#L30-L32 - validator := middleware.OapiRequestValidatorWithOptions(spec, - &middleware.Options{ - Options: openapi3filter.Options{ - AuthenticationFunc: m.authenticate, - }, - }) - return validator -} - -func (m *AuthMiddleware) authenticate(ctx context.Context, input *openapi3filter.AuthenticationInput) error { - if input.RequestValidationInput.Request.URL.Path == "/oauth/token" && input.SecuritySchemeName == "BasicAuth" { - return m.authClient(ctx, input) - } - if input.SecuritySchemeName != "ApiKeyAuth" || input.SecurityScheme.In != "cookie" { - return ErrUnsupportedAuthenticationScheme - } - return m.cookieAuth(ctx, input) -} - -func (m *AuthMiddleware) cookieAuth(_ context.Context, input *openapi3filter.AuthenticationInput) error { - _, err := api.CurrentUser(input.RequestValidationInput.Request.Context()) - return err -} - -func (m *AuthMiddleware) authClient(_ context.Context, input *openapi3filter.AuthenticationInput) error { - auth := input.RequestValidationInput.Request.Header.Get("Authorization") - if !strings.HasPrefix(auth, "Basic ") { - return fmt.Errorf("invalid basic auth header") - } - decoded, err := base64.StdEncoding.DecodeString(auth[6:]) - if err != nil { - return err - } - clientArr := strings.Split(string(decoded), ":") - if len(clientArr) != 2 { - return fmt.Errorf("invalid basic auth header") - } - client, err := m.clientRepo.FindByID(oauth.ClientID(clientArr[0])) - if err != nil { - return err - } - return client.SecretCorrect(clientArr[1]) -} - -var ErrUnsupportedAuthenticationScheme = fmt.Errorf("unsupported authentication scheme") diff --git a/internal/middleware/session.go b/internal/middleware/session.go deleted file mode 100644 index 5cf3678..0000000 --- a/internal/middleware/session.go +++ /dev/null @@ -1,18 +0,0 @@ -package middleware - -import ( - "auth/internal/api" - - "github.com/labstack/echo/v4" -) - -func Session() echo.MiddlewareFunc { - return SetCurrentUser -} - -func SetCurrentUser(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - api.SetSessionRegistry(c.Request()) - return next(c) - } -} diff --git a/internal/server/init.go b/internal/server/init.go index fcae1be..3b1dfb8 100644 --- a/internal/server/init.go +++ b/internal/server/init.go @@ -2,8 +2,8 @@ package server import ( "auth/internal/api" + "auth/internal/api/middleware" "auth/internal/di" - myMiddleware "auth/internal/middleware" "context" "os" "os/signal" @@ -12,7 +12,7 @@ import ( "github.com/kelseyhightower/envconfig" "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" + echoMid "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" ) @@ -30,20 +30,20 @@ func Init() *echo.Echo { panic(err) } e := echo.New() - e.Use(middleware.RequestID()) - e.Use(middleware.Logger()) + e.Use(echoMid.RequestID()) + e.Use(echoMid.Logger()) if l, ok := e.Logger.(*log.Logger); ok { l.SetLevel(log.INFO) } - e.Use(middleware.Recover()) - e.Use(middleware.Secure()) + e.Use(echoMid.Recover()) + e.Use(echoMid.Secure()) if config.CSRFEnabled { - e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + e.Use(echoMid.CSRFWithConfig(echoMid.CSRFConfig{ Skipper: func(c echo.Context) bool { return strings.HasPrefix(c.Path(), "/oauth") }, CookiePath: "/", })) } - e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + e.Use(echoMid.CORSWithConfig(echoMid.CORSConfig{ AllowOrigins: config.AllowOrigins, AllowHeaders: []string{ echo.HeaderOrigin, @@ -53,8 +53,8 @@ func Init() *echo.Echo { }, AllowCredentials: true, })) - e.Use(myMiddleware.Session()) e.Use(di.NewAuthMiddleware().Auth()) + e.Use(middleware.NewEchoContextMiddleware().Context()) server := di.NewServer() api.RegisterHandlers(e, api.NewStrictHandler(server, nil)) server.SetLogger(e.Logger) diff --git a/spec/main.tsp b/spec/main.tsp index 18b5c9e..602ab68 100644 --- a/spec/main.tsp +++ b/spec/main.tsp @@ -9,6 +9,7 @@ import "./specs/oauth.tsp"; import "./specs/session.tsp"; import "./specs/users.tsp"; import "./specs/account/clients.tsp"; +import "./specs/userinfo.tsp"; import "./specs/healthz.tsp"; using TypeSpec.Http; diff --git a/spec/schema/@typespec/openapi3/openapi.yaml b/spec/schema/@typespec/openapi3/openapi.yaml index d16453c..bfeb9fb 100644 --- a/spec/schema/@typespec/openapi3/openapi.yaml +++ b/spec/schema/@typespec/openapi3/openapi.yaml @@ -416,11 +416,6 @@ paths: responses: '204': description: 'There is no content to send for this request, but the headers may be useful. ' - headers: - set-cookie: - required: true - schema: - type: string '400': description: The server could not understand the request due to invalid syntax. content: @@ -448,13 +443,22 @@ paths: responses: '204': description: 'There is no content to send for this request, but the headers may be useful. ' - headers: - set-cookie: - required: true - schema: - type: string security: - ApiKeyAuth: [] + /userinfo: + get: + operationId: Userinfo_getUserinfo + summary: Get userinfo + parameters: [] + responses: + '200': + description: The request has succeeded. + content: + application/json: + schema: + $ref: '#/components/schemas/Userinfo.UserinfoRes' + security: + - BearerAuth: [] /users/signup: post: operationId: UsersInterface_signup @@ -710,6 +714,15 @@ components: format: int64 name: type: string + Userinfo.UserinfoRes: + type: object + required: + - sub + properties: + sub: + type: string + name: + type: string Users.ReqSignup: type: object required: @@ -738,6 +751,9 @@ components: BasicAuth: type: http scheme: basic + BearerAuth: + type: http + scheme: bearer servers: - url: https://auth.piny940.com/api/v1 description: Auth Server diff --git a/spec/specs/session.tsp b/spec/specs/session.tsp index 24f0043..e9c6c11 100644 --- a/spec/specs/session.tsp +++ b/spec/specs/session.tsp @@ -36,7 +36,6 @@ interface SessionInterface { @summary("Login") login(@body body: LoginReq): { @statusCode statusCode: 204; - @header setCookie: string; } | { @statusCode statusCode: 400; error: LoginErr; @@ -49,6 +48,5 @@ interface SessionInterface { @useAuth(ApiKeyAuth) logout(): { @statusCode statusCode: 204; - @header setCookie: string; }; } diff --git a/spec/specs/userinfo.tsp b/spec/specs/userinfo.tsp new file mode 100644 index 0000000..83f1b43 --- /dev/null +++ b/spec/specs/userinfo.tsp @@ -0,0 +1,25 @@ +import "@typespec/http"; +import "@typespec/rest"; +import "./model.tsp"; + +using TypeSpec.Http; +using TypeSpec.Rest; + +namespace Auth.Userinfo; + +model UserinfoRes { + sub: string; + name?: string; +} + +@route("/userinfo") +@useAuth(BearerAuth) +interface Userinfo { + @route("") + @get + @summary("Get userinfo") + getUserinfo(): { + @statusCode statusCode: 200; + @body body: UserinfoRes; + }; +} diff --git a/test/e2e/userinfo_test.go b/test/e2e/userinfo_test.go new file mode 100644 index 0000000..96c2234 --- /dev/null +++ b/test/e2e/userinfo_test.go @@ -0,0 +1,74 @@ +package e2e + +import ( + "auth/internal/api" + "auth/internal/domain" + "auth/internal/domain/oauth" + "auth/internal/infrastructure" + "auth/internal/infrastructure/model" + "auth/internal/infrastructure/query" + "fmt" + "net/http" + "slices" + "testing" + + "golang.org/x/crypto/bcrypt" +) + +func TestUserInfo(t *testing.T) { + suites := []struct { + name string + scopes []oauth.TypeScope + fields []string + }{ + {"only email", []oauth.TypeScope{oauth.ScopeEmail}, []string{}}, + {"only profile", []oauth.TypeScope{oauth.ScopeProfile}, []string{"name"}}, + {"email and profile", []oauth.TypeScope{oauth.ScopeEmail, oauth.ScopeProfile}, []string{"name"}}, + {"no scope", []oauth.TypeScope{}, []string{}}, + } + + for _, suit := range suites { + t.Run(suit.name, func(t *testing.T) { + s := newServer(t) + defer s.Close() + + db := infrastructure.GetDB() + query := query.Use(db.Client) + userID := 43829 + name := randomString(t, 10) + password := randomString(t, 16) + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + t.Fatalf("failed to generate hash: %v", err) + } + + query.User.Create(&model.User{ + ID: int64(userID), + Name: name, + EncryptedPassword: string(hash), + }) + token := accessToken(t, domain.UserID(userID), suit.scopes) + req, err := http.NewRequest(http.MethodGet, s.URL+"/userinfo", nil) + if err != nil { + t.Fatalf("failed to create request: %v", err) + } + req.Header.Set("Authorization", "Bearer "+token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatalf("failed to get: %v", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("unexpected status code: %v", resp.StatusCode) + } + resBody := &api.UserinfoUserinfoRes{} + fromJSONBody(t, resp.Body, resBody) + if resBody.Sub != fmt.Sprintf("id:%v;name:%v", userID, name) { + t.Fatalf("unexpected sub: %v", resBody.Sub) + } + if slices.Contains(suit.fields, "name") != (resBody.Name != nil) { + t.Errorf("unexpected name: %v", resBody.Name) + } + }) + } +} diff --git a/test/e2e/utils_test.go b/test/e2e/utils_test.go index 21455de..1456414 100644 --- a/test/e2e/utils_test.go +++ b/test/e2e/utils_test.go @@ -2,6 +2,9 @@ package e2e import ( "auth/internal/api" + "auth/internal/di" + "auth/internal/domain" + "auth/internal/domain/oauth" "auth/internal/infrastructure" "auth/internal/infrastructure/model" "auth/internal/infrastructure/query" @@ -15,6 +18,9 @@ import ( "net/url" "strings" "testing" + "time" + + "golang.org/x/crypto/bcrypt" ) func toJSON(t *testing.T, v interface{}) []byte { @@ -136,6 +142,44 @@ func seed( } } +func accessToken(t *testing.T, userID domain.UserID, scopes []oauth.TypeScope) string { + t.Helper() + + db := infrastructure.GetDB() + query := query.Use(db.Client) + + clientOwnerID := domain.UserID(43283) + clientOwnerName := randomString(t, 10) + password := randomString(t, 16) + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + t.Fatalf("failed to hash password: %v", err) + } + clientID := randomString(t, 16) + query.User.Create(&model.User{ + ID: int64(clientOwnerID), + Name: clientOwnerName, + EncryptedPassword: string(hash), + }) + query.Client.Create(&model.Client{ + ID: clientID, + EncryptedSecret: string(hash), + Name: "client", + UserID: int64(clientOwnerID), + }) + tokenSvc := di.NewTokenService() + token, err := tokenSvc.IssueAccessToken(&oauth.AuthCode{ + ClientID: oauth.ClientID(clientID), + UserID: userID, + AuthTime: time.Now(), + Scopes: scopes, + }) + if err != nil { + t.Fatalf("failed to issue access token: %v", err) + } + return token.Value +} + func authedGet(t *testing.T, url string, cookie *string) *http.Response { t.Helper()