{{.Title}}
{{.Message}}
package oauth import ( "context" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "html/template" "net" "net/http" "net/url" "time" httpw "github.com/eduvpn/eduvpn-common/internal/http" "github.com/eduvpn/eduvpn-common/internal/util" "github.com/eduvpn/eduvpn-common/types" ) // Generates a random base64 string to be used for state // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-4.1.1 // "state": OPTIONAL. An opaque value used by the client to maintain // state between the request and callback. The authorization server // includes this value when redirecting the user agent back to the // client. // We implement it similarly to the verifier func genState() (string, error) { randomBytes, err := util.MakeRandomByteSlice(32) if err != nil { return "", &types.WrappedErrorMessage{Message: "failed generating an OAuth state", Err: err} } // For consistency we also use raw url encoding here return base64.RawURLEncoding.EncodeToString(randomBytes), nil } // Generates a sha256 base64 challenge from a verifier // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-7.8 func genChallengeS256(verifier string) string { hash := sha256.Sum256([]byte(verifier)) // We use raw url encoding as the challenge does not accept padding return base64.RawURLEncoding.EncodeToString(hash[:]) } // Generates a verifier // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-4.1.1 // The code_verifier is a unique high-entropy cryptographically random // string generated for each authorization request, using the unreserved // characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~", with a // minimum length of 43 characters and a maximum length of 128 // characters. // We implement it according to the note: // NOTE: The code verifier SHOULD have enough entropy to make it // impractical to guess the value. It is RECOMMENDED that the output of // a suitable random number generator be used to create a 32-octet // sequence. The octet sequence is then base64url-encoded to produce a // 43-octet URL safe string to use as the code verifier. // See: https://datatracker.ietf.org/doc/html/rfc7636#section-4.1 func genVerifier() (string, error) { randomBytes, err := util.MakeRandomByteSlice(32) if err != nil { return "", &types.WrappedErrorMessage{ Message: "failed generating an OAuth verifier", Err: err, } } return base64.RawURLEncoding.EncodeToString(randomBytes), nil } type OAuth struct { Session OAuthExchangeSession `json:"-"` Token OAuthToken `json:"token"` BaseAuthorizationURL string `json:"base_authorization_url"` TokenURL string `json:"token_url"` } // This structure gets passed to the callback for easy access to the current state type OAuthExchangeSession struct { // returned from the callback CallbackError error // filled in in initialize ClientID string ISS string State string Verifier string // filled in when constructing the callback Context context.Context Server *http.Server Listener net.Listener } // Struct that defines the json format for /.well-known/vpn-user-portal" type OAuthToken struct { Access string `json:"access_token"` Refresh string `json:"refresh_token"` Type string `json:"token_type"` Expires int64 `json:"expires_in"` ExpiredTimestamp time.Time `json:"expires_in_timestamp"` } // Sets up a listener func (oauth *OAuth) setupListener() error { errorMessage := "failed setting up listener" oauth.Session.Context = context.Background() // create a listener listener, listenerErr := net.Listen("tcp", ":0") if listenerErr != nil { return &types.WrappedErrorMessage{Message: errorMessage, Err: listenerErr} } oauth.Session.Listener = listener return nil } func (oauth *OAuth) getTokensWithCallback() error { errorMessage := "failed getting tokens with callback" if oauth.Session.Listener == nil { return &types.WrappedErrorMessage{Message: errorMessage, Err: errors.New("No listener")} } mux := http.NewServeMux() // server /callback over the listener address oauth.Session.Server = &http.Server{ Handler: mux, } mux.HandleFunc("/callback", oauth.Callback) if err := oauth.Session.Server.Serve(oauth.Session.Listener); err != http.ErrServerClosed { return &types.WrappedErrorMessage{Message: errorMessage, Err: err} } return oauth.Session.CallbackError } // Get the access and refresh tokens // Access tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.4 // Refresh tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.3.2 func (oauth *OAuth) getTokensWithAuthCode(authCode string) error { errorMessage := "failed getting tokens with the authorization code" // Make sure the verifier is set as the parameter // so that the server can verify that we are the actual owner of the authorization code reqURL := oauth.TokenURL port, portErr := oauth.GetListenerPort() if portErr != nil { return &types.WrappedErrorMessage{Message: errorMessage, Err: portErr} } data := url.Values{ "client_id": {oauth.Session.ClientID}, "code": {authCode}, "code_verifier": {oauth.Session.Verifier}, "grant_type": {"authorization_code"}, "redirect_uri": {fmt.Sprintf("http://127.0.0.1:%d/callback", port)}, } headers := http.Header{ "content-type": {"application/x-www-form-urlencoded"}, } opts := &httpw.HTTPOptionalParams{Headers: headers, Body: data} current_time := util.GetCurrentTime() _, body, bodyErr := httpw.HTTPPostWithOpts(reqURL, opts) if bodyErr != nil { return &types.WrappedErrorMessage{Message: errorMessage, Err: bodyErr} } tokenStructure := OAuthToken{} jsonErr := json.Unmarshal(body, &tokenStructure) if jsonErr != nil { return &types.WrappedErrorMessage{ Message: errorMessage, Err: &httpw.HTTPParseJsonError{URL: reqURL, Body: string(body), Err: jsonErr}, } } tokenStructure.ExpiredTimestamp = current_time.Add( time.Second * time.Duration(tokenStructure.Expires), ) oauth.Token = tokenStructure return nil } func (oauth *OAuth) isTokensExpired() bool { expired_time := oauth.Token.ExpiredTimestamp current_time := util.GetCurrentTime() return !current_time.Before(expired_time) } // Get the access and refresh tokens with a previously received refresh token // Access tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.4 // Refresh tokens: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-04#section-1.3.2 func (oauth *OAuth) getTokensWithRefresh() error { errorMessage := "failed getting tokens with the refresh token" reqURL := oauth.TokenURL data := url.Values{ "refresh_token": {oauth.Token.Refresh}, "grant_type": {"refresh_token"}, } headers := http.Header{ "content-type": {"application/x-www-form-urlencoded"}, } opts := &httpw.HTTPOptionalParams{Headers: headers, Body: data} current_time := util.GetCurrentTime() _, body, bodyErr := httpw.HTTPPostWithOpts(reqURL, opts) if bodyErr != nil { return &types.WrappedErrorMessage{Message: errorMessage, Err: bodyErr} } tokenStructure := OAuthToken{} jsonErr := json.Unmarshal(body, &tokenStructure) if jsonErr != nil { return &types.WrappedErrorMessage{ Message: errorMessage, Err: &httpw.HTTPParseJsonError{URL: reqURL, Body: string(body), Err: jsonErr}, } } tokenStructure.ExpiredTimestamp = current_time.Add( time.Second * time.Duration(tokenStructure.Expires), ) oauth.Token = tokenStructure return nil } // Adapted from: https://github.com/eduvpn/apple/blob/5b18f834be7aebfed00570ae0c2f7bcbaf1c69cc/EduVPN/Helpers/Mac/OAuthRedirectHTTPHandler.m#L25 const responseTemplate string = `
{{.Message}}