oauth2 icon indicating copy to clipboard operation
oauth2 copied to clipboard

Optional & Secured JSON response with Redirect URL for code flow

Open willjleong opened this issue 5 years ago • 0 comments

Problem

  • Right now the /authorize handler forces a redirect as the response when valid. If the request comes client-side from another domain, the redirect fails because of web security standards. For modern web-apps the frontend can commonly be a universal-rendered-js app using a framework like next or nuxt. A developer may want to implement the authorize flow in the seperate frontend project to preserve the look & feel as well as a potential to use the user's existing session or JWT so they don't have to login again when authorizing a oauth2 client.

Proposal

  • Proposal: Allow an option for a non-redirect (JSON response) with the sensitive redirectURL from authorize when using the code response_type. To lock down the security, there could be a configuration for whitelisting certain domains / origins (similar to CORS) so that malicious devs couldn't imitate this flow to leak access tokens.

I ended up implementing a workaround for the stack I've been working with where the Frontend is a NextJS universal react app and the backend written in golang. Something like this for the Authorize handler (my ugly override):

func (s *CustomOauthServer) CustomHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) error {
	req, err := s.ValidationAuthorizeRequest(r)
	if err != nil {
		return s.redirectError(w, req, err)
	}

	// user authorization
	userID, err := s.UserAuthorizationHandler(w, r)
	if err != nil {
		return s.redirectError(w, req, err)
	} else if userID == "" {
		return nil
	}
	req.UserID = userID

	// specify the scope of authorization
	if fn := s.AuthorizeScopeHandler; fn != nil {
		scope, err := fn(w, r)
		if err != nil {
			return err
		} else if scope != "" {
			req.Scope = scope
		}
	}

	// specify the expiration time of access token
	if fn := s.AccessTokenExpHandler; fn != nil {
		exp, err := fn(w, r)
		if err != nil {
			return err
		}
		req.AccessTokenExp = exp
	}

	ti, err := s.GetAuthorizeToken(req)
	if err != nil {
		return s.redirectError(w, req, err)
	}

	// If the redirect URI is empty, the default domain provided by the client is used.
	if req.RedirectURI == "" {
		client, err := s.Manager.GetClient(req.ClientID)
		if err != nil {
			return err
		}
		req.RedirectURI = client.GetDomain()
	}

	crd := r.FormValue("cancel_redirect")
	responseData := s.GetAuthorizeData(req.ResponseType, ti)
	if crd == "" {
		err := s.redirect(w, req, responseData)
		return err
	} else {
		uri, err := s.GetRedirectURI(req, responseData)
		if err != nil {
			return err
		}
                // Here would be URL whitelist check would happen here and reject origin domains not in config.
		data := map[string]interface{}{"redirect_url": uri}
		err = s.noRedirect(w, data, nil)
		return err
	}
}

func (s *CustomOauthServer) noRedirect(w http.ResponseWriter, data map[string]interface{}, header http.Header, statusCode ...int) error {
	w.Header().Set("Content-Type", "application/json;charset=UTF-8")
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("Pragma", "no-cache")

	for key := range header {
		w.Header().Set(key, header.Get(key))
	}

	status := http.StatusOK
	if len(statusCode) > 0 && statusCode[0] > 0 {
		status = statusCode[0]
	}

	w.WriteHeader(status)
	return json.NewEncoder(w).Encode(data)
}

Anyone else think this feature might be useful / helpful? I ended up extending the Oauth2Server to workaround the current status of the library.

willjleong avatar Dec 09 '19 23:12 willjleong