| // Copyright Istio Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package server |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "net" |
| "net/http" |
| "net/http/httputil" |
| "time" |
| ) |
| |
| import ( |
| "istio.io/pkg/log" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-go-pixiu/pkg/security" |
| "github.com/apache/dubbo-go-pixiu/security/pkg/stsservice" |
| ) |
| |
| const ( |
| // TokenPath is url path for handling STS requests. |
| TokenPath = "/token" |
| // StsStatusPath is the path for dumping STS status. |
| StsStatusPath = "/stsStatus" |
| // URLEncodedForm is the encoding type specified in a STS request. |
| URLEncodedForm = "application/x-www-form-urlencoded" |
| // TokenExchangeGrantType is the required value for "grant_type" parameter in a STS request. |
| TokenExchangeGrantType = "urn:ietf:params:oauth:grant-type:token-exchange" |
| // SubjectTokenType is the required token type in a STS request. |
| SubjectTokenType = "urn:ietf:params:oauth:token-type:jwt" |
| ) |
| |
| var stsServerLog = log.RegisterScope("stsserver", "STS service debugging", 0) |
| |
| // error code sent in a STS error response. A full list of error code is |
| // defined in https://tools.ietf.org/html/rfc6749#section-5.2. |
| const ( |
| // If the request itself is not valid or if either the "subject_token" or |
| // "actor_token" are invalid or unacceptable, the STS server must set |
| // error code to "invalid_request". |
| invalidRequest = "invalid_request" |
| // If the authorization server is unwilling or unable to issue a token, the |
| // STS server should set error code to "invalid_target". |
| invalidTarget = "invalid_target" |
| ) |
| |
| // Server watches HTTP requests for security token service (STS), and returns |
| // token in response. |
| type Server struct { |
| // tokenManager takes STS request parameters and generates tokens, and returns |
| // generated token to the STS server. |
| tokenManager security.TokenManager |
| stsServer *http.Server |
| // Port number that server listens on. |
| Port int |
| } |
| |
| // Config for the STS server. |
| type Config struct { |
| LocalHostAddr string |
| LocalPort int |
| } |
| |
| // NewServer creates a new STS server. |
| func NewServer(config Config, tokenManager security.TokenManager) (*Server, error) { |
| s := &Server{ |
| tokenManager: tokenManager, |
| } |
| mux := http.NewServeMux() |
| mux.HandleFunc(TokenPath, s.ServeStsRequests) |
| mux.HandleFunc(StsStatusPath, s.DumpStsStatus) |
| s.stsServer = &http.Server{ |
| Addr: fmt.Sprintf("%s:%d", config.LocalHostAddr, config.LocalPort), |
| Handler: mux, |
| IdleTimeout: 90 * time.Second, // matches http.DefaultTransport keep-alive timeout |
| ReadTimeout: 30 * time.Second, |
| } |
| ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", config.LocalHostAddr, config.LocalPort)) |
| if err != nil { |
| log.Errorf("Server failed to listen %v", err) |
| return nil, err |
| } |
| // If passed in port is 0, get the actual chosen port. |
| s.Port = ln.Addr().(*net.TCPAddr).Port |
| go func() { |
| stsServerLog.Infof("Start listening on %s:%d", config.LocalHostAddr, s.Port) |
| err := s.stsServer.Serve(ln) |
| // ListenAndServe always returns a non-nil error. |
| stsServerLog.Error(err) |
| }() |
| return s, nil |
| } |
| |
| // ServeStsRequests handles STS requests and sends exchanged token in responses. |
| func (s *Server) ServeStsRequests(w http.ResponseWriter, req *http.Request) { |
| reqParam, validationError := s.validateStsRequest(req) |
| if validationError != nil { |
| stsServerLog.Warnf("STS request is invalid: %v", validationError) |
| // If request is invalid, the error code must be "invalid_request". |
| // https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16#section-2.2.2. |
| s.sendErrorResponse(w, invalidRequest, validationError) |
| return |
| } |
| tokenDataJSON, genError := s.tokenManager.GenerateToken(reqParam) |
| if genError != nil { |
| stsServerLog.Warnf("token manager fails to generate token: %v", genError) |
| // If the authorization server is unable to issue a token, the "invalid_target" error code |
| // should be used in the error response. |
| // https://tools.ietf.org/html/draft-ietf-oauth-token-exchange-16#section-2.2.2. |
| s.sendErrorResponse(w, invalidTarget, genError) |
| return |
| } |
| s.sendSuccessfulResponse(w, tokenDataJSON) |
| } |
| |
| // validateStsRequest validates a STS request, and extracts STS parameters from the request. |
| func (s *Server) validateStsRequest(req *http.Request) (security.StsRequestParameters, error) { |
| reqParam := security.StsRequestParameters{} |
| if req == nil { |
| return reqParam, errors.New("request is nil") |
| } |
| |
| if stsServerLog.DebugEnabled() { |
| reqDump, _ := httputil.DumpRequest(req, true) |
| stsServerLog.Debugf("Received STS request: %s", string(reqDump)) |
| } |
| if req.Method != "POST" { |
| return reqParam, fmt.Errorf("request method is invalid, should be POST but get %s", req.Method) |
| } |
| if req.Header.Get("Content-Type") != URLEncodedForm { |
| return reqParam, fmt.Errorf("request content type is invalid, should be %s but get %s", URLEncodedForm, |
| req.Header.Get("Content-type")) |
| } |
| if parseErr := req.ParseForm(); parseErr != nil { |
| return reqParam, fmt.Errorf("failed to parse query from STS request: %v", parseErr) |
| } |
| if req.PostForm.Get("grant_type") != TokenExchangeGrantType { |
| return reqParam, fmt.Errorf("request query grant_type is invalid, should be %s but get %s", |
| TokenExchangeGrantType, req.PostForm.Get("grant_type")) |
| } |
| // Only a JWT token is accepted. |
| if req.PostForm.Get("subject_token") == "" { |
| return reqParam, errors.New("subject_token is empty") |
| } |
| if req.PostForm.Get("subject_token_type") != SubjectTokenType { |
| return reqParam, fmt.Errorf("subject_token_type is invalid, should be %s but get %s", |
| SubjectTokenType, req.PostForm.Get("subject_token_type")) |
| } |
| reqParam.GrantType = req.PostForm.Get("grant_type") |
| reqParam.Resource = req.PostForm.Get("resource") |
| reqParam.Audience = req.PostForm.Get("audience") |
| reqParam.Scope = req.PostForm.Get("scope") |
| reqParam.RequestedTokenType = req.PostForm.Get("requested_token_type") |
| reqParam.SubjectToken = req.PostForm.Get("subject_token") |
| reqParam.SubjectTokenType = req.PostForm.Get("subject_token_type") |
| reqParam.ActorToken = req.PostForm.Get("actor_token") |
| reqParam.ActorTokenType = req.PostForm.Get("actor_token_type") |
| return reqParam, nil |
| } |
| |
| // sendErrorResponse takes error type and error details, generates an error response and sends out. |
| func (s *Server) sendErrorResponse(w http.ResponseWriter, errorType string, errDetail error) { |
| w.Header().Add("Content-Type", "application/json") |
| if errorType == invalidRequest { |
| w.WriteHeader(http.StatusBadRequest) |
| } else { |
| w.WriteHeader(http.StatusInternalServerError) |
| } |
| errResp := stsservice.StsErrorResponse{ |
| Error: errorType, |
| ErrorDescription: errDetail.Error(), |
| } |
| if errRespJSON, err := json.MarshalIndent(errResp, "", " "); err == nil { |
| if _, err := w.Write(errRespJSON); err != nil { |
| stsServerLog.Errorf("failure in sending STS error response (%v): %v", errResp, err) |
| return |
| } |
| stsServerLog.Debugf("sent out STS error response: %v", errResp) |
| } else { |
| stsServerLog.Errorf("failure in marshaling error response (%v) into JSON: %v", errResp, err) |
| } |
| } |
| |
| // sendSuccessfulResponse takes token data and generates a successful STS response, and sends out the STS response. |
| func (s *Server) sendSuccessfulResponse(w http.ResponseWriter, tokenData []byte) { |
| w.Header().Add("Content-Type", "application/json") |
| w.WriteHeader(http.StatusOK) |
| if _, err := w.Write(tokenData); err != nil { |
| stsServerLog.Errorf("failure in sending STS success response: %v", err) |
| return |
| } |
| stsServerLog.Debug("sent out STS success response") |
| } |
| |
| // DumpStsStatus handles requests for dumping STS status, including STS requests being served, |
| // tokens being fetched. |
| func (s *Server) DumpStsStatus(w http.ResponseWriter, req *http.Request) { |
| if stsServerLog.DebugEnabled() { |
| reqDump, _ := httputil.DumpRequest(req, true) |
| stsServerLog.Debugf("Received STS request: %s", string(reqDump)) |
| } |
| |
| stsStatusJSON, err := s.tokenManager.DumpTokenStatus() |
| if err != nil { |
| stsServerLog.Errorf("token manager failed at dumping token status: %v", err) |
| w.WriteHeader(http.StatusInternalServerError) |
| failureMessage := fmt.Sprintf("failure in dumping STS server status: %v", err) |
| if _, err := w.Write([]byte(failureMessage)); err != nil { |
| stsServerLog.Errorf("failure in sending error response to a STS dump request: %v", err) |
| } |
| return |
| } |
| w.Header().Add("Content-Type", "application/json") |
| if _, err := w.Write(stsStatusJSON); err != nil { |
| stsServerLog.Errorf("failure in sending STS status dump: %v", err) |
| return |
| } |
| stsServerLog.Debug("sent out STS status dump") |
| } |
| |
| // Stop closes the server |
| func (s *Server) Stop() { |
| if err := s.stsServer.Shutdown(context.TODO()); err != nil { |
| stsServerLog.Errorf("failed to shut down STS server: %v", err) |
| } |
| } |