@ -6,9 +6,10 @@ import (
"fmt"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/guregu/null/zero "
"github.com/writeas/impart "
"github.com/writeas/nerds/store"
"github.com/writeas/web-core/auth"
"github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"io"
"io/ioutil"
@ -23,6 +24,7 @@ type TokenResponse struct {
ExpiresIn int ` json:"expires_in" `
RefreshToken string ` json:"refresh_token" `
TokenType string ` json:"token_type" `
Error string ` json:"error" `
}
// InspectResponse contains data returned when an access token is inspected.
@ -31,7 +33,9 @@ type InspectResponse struct {
UserID string ` json:"user_id" `
ExpiresAt time . Time ` json:"expires_at" `
Username string ` json:"username" `
DisplayName string ` json:"-" `
Email string ` json:"email" `
Error string ` json:"error" `
}
// tokenRequestMaxLen is the most bytes that we'll read from the /oauth/token
@ -59,7 +63,7 @@ type OAuthDatastore interface {
GenerateOAuthState ( context . Context , string , string ) ( string , error )
CreateUser ( * config . Config , * User , string ) error
GetUserForAuth ByID ( int64 ) ( * User , error )
GetUserByID ( int64 ) ( * User , error )
}
type HttpClient interface {
@ -78,63 +82,64 @@ type oauthHandler struct {
Config * config . Config
DB OAuthDatastore
Store sessions . Store
EmailKey [ ] byte
oauthClient oauthClient
}
func ( h oauthHandler ) viewOauthInit ( w http . ResponseWriter , r * http . Request ) {
func ( h oauthHandler ) viewOauthInit ( app * App , w http . ResponseWriter , r * http . Request ) error {
ctx := r . Context ( )
state , err := h . DB . GenerateOAuthState ( ctx , h . oauthClient . GetProvider ( ) , h . oauthClient . GetClientID ( ) )
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , "could not prepare oauth redirect url" )
return impart . HTTPError { http . StatusInternalServerError , "could not prepare oauth redirect url" }
}
location , err := h . oauthClient . buildLoginURL ( state )
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , "could not prepare oauth redirect url" )
return
return impart . HTTPError { http . StatusInternalServerError , "could not prepare oauth redirect url" }
}
http . Redirect ( w , r , location , http . StatusTemporaryRedirect )
return impart . HTTPError { http . StatusTemporaryRedirect , location }
}
func configureSlackOauth ( r * mux . Router , app * App ) {
func configureSlackOauth ( parentHandler * Handler , r * mux . Router , app * App ) {
if app . Config ( ) . SlackOauth . ClientID != "" {
oauthClient := slackOauthClient {
ClientID : app . Config ( ) . SlackOauth . ClientID ,
ClientSecret : app . Config ( ) . SlackOauth . ClientSecret ,
TeamID : app . Config ( ) . SlackOauth . TeamID ,
CallbackLocation : app . Config ( ) . App . Host + "/oauth/callback" ,
HttpClient : & http . Client { Timeout : 10 * time . Second } ,
HttpClient : config . DefaultHTTPClient ( ) ,
}
configureOauthRoutes ( r , app , oauthClient )
configureOauthRoutes ( parentHandler , r , app , oauthClient )
}
}
func configureWriteAsOauth ( r * mux . Router , app * App ) {
func configureWriteAsOauth ( parentHandler * Handler , r * mux . Router , app * App ) {
if app . Config ( ) . WriteAsOauth . ClientID != "" {
oauthClient := writeAsOauthClient {
ClientID : app . Config ( ) . WriteAsOauth . ClientID ,
ClientSecret : app . Config ( ) . WriteAsOauth . ClientSecret ,
ExchangeLocation : app . Config ( ) . WriteAsOauth . TokenLocation ,
InspectLocation : app . Config ( ) . WriteAsOauth . InspectLocation ,
AuthLocation : app . Config ( ) . WriteAsOauth . AuthLocation ,
HttpClient : & http . Client { Timeout : 10 * time . Second } ,
ExchangeLocation : config . OrDefaultString ( app . Config ( ) . WriteAsOauth . TokenLocation , writeAsExchangeLocation ) ,
InspectLocation : config . OrDefaultString ( app . Config ( ) . WriteAsOauth . InspectLocation , writeAsIdentityLocation ) ,
AuthLocation : config . OrDefaultString ( app . Config ( ) . WriteAsOauth . AuthLocation , writeAsAuthLocation ) ,
HttpClient : config . DefaultHTTPClient ( ) ,
CallbackLocation : app . Config ( ) . App . Host + "/oauth/callback" ,
}
configureOauthRoutes ( r , app , oauthClient )
configureOauthRoutes ( parentHandler , r , app , oauthClient )
}
}
func configureOauthRoutes ( r * mux . Router , app * App , oauthClient oauthClient ) {
func configureOauthRoutes ( parentHandler * Handler , r * mux . Router , app * App , oauthClient oauthClient ) {
handler := & oauthHandler {
Config : app . Config ( ) ,
DB : app . DB ( ) ,
Store : app . SessionStore ( ) ,
oauthClient : oauthClient ,
EmailKey : app . keys . EmailKey ,
}
r . HandleFunc ( "/oauth/" + oauthClient . GetProvider ( ) , handler . viewOauthInit ) . Methods ( "GET" )
r . HandleFunc ( "/oauth/callback" , handler . viewOauthCallback ) . Methods ( "GET" )
r . HandleFunc ( "/oauth/" + oauthClient . GetProvider ( ) , parentHandler . OAuth ( handler . viewOauthInit ) ) . Methods ( "GET" )
r . HandleFunc ( "/oauth/callback" , parentHandler . OAuth ( handler . viewOauthCallback ) ) . Methods ( "GET" )
}
func ( h oauthHandler ) viewOauthCallback ( w http . ResponseWriter , r * http . Request ) {
func ( h oauthHandler ) viewOauthCallback ( app * App , w http . ResponseWriter , r * http . Request ) error {
ctx := r . Context ( )
code := r . FormValue ( "code" )
@ -142,28 +147,28 @@ func (h oauthHandler) viewOauthCallback(w http.ResponseWriter, r *http.Request)
provider , clientID , err := h . DB . ValidateOAuthState ( ctx , state )
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , err . Error ( ) )
return
log . Error ( "Unable to ValidateOAuthState: %s" , err )
return impart . HTTPError { http . StatusInternalServerError , err . Error ( ) }
}
tokenResponse , err := h . oauthClient . exchangeOauthCode ( ctx , code )
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , err . Error ( ) )
return
log . Error ( "Unable to exchangeOauthCode: %s" , err )
return impart . HTTPError { http . StatusInternalServerError , err . Error ( ) }
}
// Now that we have the access token, let's use it real quick to make sur
// it really really works.
tokenInfo , err := h . oauthClient . inspectOauthAccessToken ( ctx , tokenResponse . AccessToken )
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , err . Error ( ) )
return
log . Error ( "Unable to inspectOauthAccessToken: %s" , err )
return impart . HTTPError { http . StatusInternalServerError , err . Error ( ) }
}
localUserID , err := h . DB . GetIDForRemoteUser ( ctx , tokenInfo . UserID , provider , clientID )
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , err . Error ( ) )
return
log . Error ( "Unable to GetIDForRemoteUser: %s" , err )
return impart . HTTPError { http . StatusInternalServerError , err . Error ( ) }
}
if localUserID == - 1 {
@ -174,43 +179,44 @@ func (h oauthHandler) viewOauthCallback(w http.ResponseWriter, r *http.Request)
randPass := store . Generate62RandomString ( 14 )
hashedPass , err := auth . HashPass ( [ ] byte ( randPass ) )
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , "unable to create password hash" )
return
return impart . HTTPError { http . StatusInternalServerError , "unable to create password hash" }
}
newUser := & User {
Username : tokenInfo . Username ,
HashedPass : hashedPass ,
HasPass : true ,
Email : zero . NewString ( tokenInfo . Email , tokenInfo . Email != "" ) ,
Email : prepareUserEmail ( tokenInfo . Email , h . EmailKey ) ,
Created : time . Now ( ) . Truncate ( time . Second ) . UTC ( ) ,
}
displayName := tokenInfo . DisplayName
if len ( displayName ) == 0 {
displayName = tokenInfo . Username
}
err = h . DB . CreateUser ( h . Config , newUser , newUser . Username )
err = h . DB . CreateUser ( h . Config , newUser , displayN ame)
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , err . Error ( ) )
return
return impart . HTTPError { http . StatusInternalServerError , err . Error ( ) }
}
err = h . DB . RecordRemoteUserID ( ctx , newUser . ID , tokenInfo . UserID , provider , clientID , tokenResponse . AccessToken )
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , err . Error ( ) )
return
return impart . HTTPError { http . StatusInternalServerError , err . Error ( ) }
}
if err := loginOrFail ( h . Store , w , r , newUser ) ; err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , err . Error ( ) )
return impart . HTTPError { http . StatusInternalServerError , err . Error ( ) }
}
return
return nil
}
user , err := h . DB . GetUserForAuth ByID ( localUserID )
user , err := h . DB . GetUserByID ( localUserID )
if err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , err . Error ( ) )
return
return impart . HTTPError { http . StatusInternalServerError , err . Error ( ) }
}
if err = loginOrFail ( h . Store , w , r , user ) ; err != nil {
failOAuthRequest ( w , http . StatusInternalServerError , err . Error ( ) )
return impart . HTTPError { http . StatusInternalServerError , err . Error ( ) }
}
return nil
}
func limitedJsonUnmarshal ( body io . ReadCloser , n int , thing interface { } ) error {
@ -236,16 +242,3 @@ func loginOrFail(store sessions.Store, w http.ResponseWriter, r *http.Request, u
http . Redirect ( w , r , "/" , http . StatusTemporaryRedirect )
return nil
}
// failOAuthRequest is an HTTP handler helper that formats returned error
// messages.
func failOAuthRequest ( w http . ResponseWriter , statusCode int , message string ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . WriteHeader ( statusCode )
err := json . NewEncoder ( w ) . Encode ( map [ string ] interface { } {
"error" : message ,
} )
if err != nil {
panic ( err )
}
}