2021-03-14 16:56:16 +00:00
/ *
GoToSocial
Copyright ( C ) 2021 GoToSocial Authors admin @ gotosocial . org
This program is free software : you can redistribute it and / or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation , either version 3 of the License , or
( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
GNU Affero General Public License for more details .
You should have received a copy of the GNU Affero General Public License
along with this program . If not , see < http : //www.gnu.org/licenses/>.
* /
package oauth
2021-03-15 17:59:38 +00:00
import (
2021-03-20 18:06:28 +00:00
"fmt"
2021-03-17 10:33:06 +00:00
"net/http"
"net/url"
2021-03-17 12:14:52 +00:00
"github.com/gin-contrib/sessions"
2021-03-17 10:33:06 +00:00
"github.com/gin-gonic/gin"
2021-03-15 22:05:24 +00:00
"github.com/go-pg/pg/v10"
2021-03-20 18:06:28 +00:00
"github.com/google/uuid"
2021-03-15 22:05:24 +00:00
"github.com/gotosocial/gotosocial/internal/api"
"github.com/gotosocial/gotosocial/internal/gtsmodel"
2021-03-18 22:27:43 +00:00
"github.com/gotosocial/gotosocial/pkg/mastotypes"
2021-03-15 17:59:38 +00:00
"github.com/gotosocial/oauth2/v4"
"github.com/gotosocial/oauth2/v4/errors"
"github.com/gotosocial/oauth2/v4/manage"
"github.com/gotosocial/oauth2/v4/server"
"github.com/sirupsen/logrus"
2021-03-15 22:05:24 +00:00
"golang.org/x/crypto/bcrypt"
2021-03-15 17:59:38 +00:00
)
2021-03-20 22:30:19 +00:00
const (
outOfBandRedirect = "urn:ietf:wg:oauth:2.0:oob"
appsPath = "/api/v1/apps"
authSignInPath = "/auth/sign_in"
oauthTokenPath = "/oauth/token"
oauthAuthorizePath = "/oauth/authorize"
)
2021-03-15 17:59:38 +00:00
type API struct {
manager * manage . Manager
server * server . Server
2021-03-15 22:05:24 +00:00
conn * pg . DB
log * logrus . Logger
2021-03-15 15:15:14 +00:00
}
2021-03-17 15:01:31 +00:00
type login struct {
2021-03-18 22:27:43 +00:00
Email string ` form:"username" `
2021-03-17 15:01:31 +00:00
Password string ` form:"password" `
}
2021-03-18 22:27:43 +00:00
type code struct {
Code string ` form:"code" `
2021-03-17 15:01:31 +00:00
}
2021-03-15 22:05:24 +00:00
func New ( ts oauth2 . TokenStore , cs oauth2 . ClientStore , conn * pg . DB , log * logrus . Logger ) * API {
2021-03-15 17:59:38 +00:00
manager := manage . NewDefaultManager ( )
manager . MapTokenStorage ( ts )
manager . MapClientStorage ( cs )
2021-03-17 18:52:36 +00:00
manager . SetAuthorizeCodeTokenCfg ( manage . DefaultAuthorizeCodeTokenCfg )
sc := & server . Config {
TokenType : "Bearer" ,
// Must follow the spec.
AllowGetAccessRequest : false ,
// Support only the non-implicit flow.
AllowedResponseTypes : [ ] oauth2 . ResponseType { oauth2 . Code } ,
// Allow:
// - Authorization Code (for first & third parties)
// - Refreshing Tokens
//
// Deny:
// - Resource owner secrets (password grant)
// - Client secrets
AllowedGrantTypes : [ ] oauth2 . GrantType {
oauth2 . AuthorizationCode ,
oauth2 . Refreshing ,
} ,
2021-03-18 22:27:43 +00:00
AllowedCodeChallengeMethods : [ ] oauth2 . CodeChallengeMethod {
oauth2 . CodeChallengePlain ,
} ,
2021-03-17 18:52:36 +00:00
}
2021-03-15 17:59:38 +00:00
2021-03-17 18:52:36 +00:00
srv := server . NewServer ( sc , manager )
2021-03-15 17:59:38 +00:00
srv . SetInternalErrorHandler ( func ( err error ) * errors . Response {
log . Errorf ( "internal oauth error: %s" , err )
return nil
} )
srv . SetResponseErrorHandler ( func ( re * errors . Response ) {
log . Errorf ( "internal response error: %s" , re . Error )
} )
2021-03-17 10:33:06 +00:00
api := & API {
2021-03-15 17:59:38 +00:00
manager : manager ,
server : srv ,
2021-03-15 22:05:24 +00:00
conn : conn ,
log : log ,
2021-03-15 17:59:38 +00:00
}
2021-03-17 10:33:06 +00:00
api . server . SetUserAuthorizationHandler ( api . UserAuthorizationHandler )
api . server . SetClientInfoHandler ( server . ClientFormHandler )
return api
2021-03-14 16:56:16 +00:00
}
2021-03-15 22:05:24 +00:00
func ( a * API ) AddRoutes ( s api . Server ) error {
2021-03-20 22:30:19 +00:00
s . AttachHandler ( http . MethodPost , appsPath , a . AppsPOSTHandler )
2021-03-20 18:06:28 +00:00
2021-03-20 22:30:19 +00:00
s . AttachHandler ( http . MethodGet , authSignInPath , a . SignInGETHandler )
s . AttachHandler ( http . MethodPost , authSignInPath , a . SignInPOSTHandler )
2021-03-18 22:27:43 +00:00
2021-03-20 22:30:19 +00:00
s . AttachHandler ( http . MethodPost , oauthTokenPath , a . TokenPOSTHandler )
2021-03-18 22:27:43 +00:00
2021-03-20 22:30:19 +00:00
s . AttachHandler ( http . MethodGet , oauthAuthorizePath , a . AuthorizeGETHandler )
s . AttachHandler ( http . MethodPost , oauthAuthorizePath , a . AuthorizePOSTHandler )
2021-03-18 22:27:43 +00:00
2021-03-15 22:05:24 +00:00
return nil
}
func incorrectPassword ( ) ( string , error ) {
return "" , errors . New ( "password/email combination was incorrect" )
}
2021-03-17 10:33:06 +00:00
/ *
MAIN HANDLERS -- serve these through a server / router
* /
2021-03-20 18:05:37 +00:00
// AppsPOSTHandler should be served at https://example.org/api/v1/apps
// It is equivalent to: https://docs.joinmastodon.org/methods/apps/
func ( a * API ) AppsPOSTHandler ( c * gin . Context ) {
l := a . log . WithField ( "func" , "AppsPOSTHandler" )
l . Trace ( "entering AppsPOSTHandler" )
form := & mastotypes . ApplicationPOSTRequest { }
if err := c . ShouldBind ( form ) ; err != nil {
c . JSON ( http . StatusUnprocessableEntity , gin . H { "error" : err . Error ( ) } )
return
}
// permitted length for most fields
permittedLength := 64
// redirect can be a bit bigger because we probably need to encode data in the redirect uri
permittedRedirect := 256
// check lengths of fields before proceeding so the user can't spam huge entries into the database
if len ( form . ClientName ) > permittedLength {
c . JSON ( http . StatusBadRequest , gin . H { "error" : fmt . Sprintf ( "client_name must be less than %d bytes" , permittedLength ) } )
return
}
if len ( form . Website ) > permittedLength {
c . JSON ( http . StatusBadRequest , gin . H { "error" : fmt . Sprintf ( "website must be less than %d bytes" , permittedLength ) } )
return
}
if len ( form . RedirectURIs ) > permittedRedirect {
c . JSON ( http . StatusBadRequest , gin . H { "error" : fmt . Sprintf ( "redirect_uris must be less than %d bytes" , permittedRedirect ) } )
return
}
if len ( form . Scopes ) > permittedLength {
c . JSON ( http . StatusBadRequest , gin . H { "error" : fmt . Sprintf ( "scopes must be less than %d bytes" , permittedLength ) } )
return
}
// set default 'read' for scopes if it's not set
var scopes string
if form . Scopes == "" {
scopes = "read"
} else {
scopes = form . Scopes
}
// generate new IDs for this application and its associated client
clientID := uuid . NewString ( )
clientSecret := uuid . NewString ( )
vapidKey := uuid . NewString ( )
// generate the application to put in the database
app := & gtsmodel . Application {
Name : form . ClientName ,
Website : form . Website ,
RedirectURI : form . RedirectURIs ,
ClientID : clientID ,
ClientSecret : clientSecret ,
Scopes : scopes ,
VapidKey : vapidKey ,
}
// chuck it in the db
if _ , err := a . conn . Model ( app ) . Insert ( ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
return
}
// now we need to model an oauth client from the application that the oauth library can use
oc := & oauthClient {
ID : clientID ,
Secret : clientSecret ,
Domain : form . RedirectURIs ,
UserID : "" , // This client isn't yet associated with a specific user, it's just an app client right now
}
// chuck it in the db
if _ , err := a . conn . Model ( oc ) . Insert ( ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
return
}
// done, return the new app information per the spec here: https://docs.joinmastodon.org/methods/apps/
c . JSON ( http . StatusOK , app )
}
2021-03-17 15:01:31 +00:00
// SignInGETHandler should be served at https://example.org/auth/sign_in.
// The idea is to present a sign in page to the user, where they can enter their username and password.
// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler
func ( a * API ) SignInGETHandler ( c * gin . Context ) {
2021-03-17 18:52:36 +00:00
a . log . WithField ( "func" , "SignInGETHandler" ) . Trace ( "serving sign in html" )
2021-03-18 22:27:43 +00:00
c . HTML ( http . StatusOK , "sign-in.tmpl" , gin . H { } )
2021-03-17 15:01:31 +00:00
}
// SignInPOSTHandler should be served at https://example.org/auth/sign_in.
2021-03-17 10:33:06 +00:00
// The idea is to present a sign in page to the user, where they can enter their username and password.
// The handler will then redirect to the auth handler served at /auth
2021-03-17 15:01:31 +00:00
func ( a * API ) SignInPOSTHandler ( c * gin . Context ) {
2021-03-17 18:52:36 +00:00
l := a . log . WithField ( "func" , "SignInPOSTHandler" )
2021-03-17 12:14:52 +00:00
s := sessions . Default ( c )
2021-03-17 15:01:31 +00:00
form := & login { }
2021-03-18 22:27:43 +00:00
if err := c . ShouldBind ( form ) ; err != nil {
2021-03-17 15:01:31 +00:00
c . JSON ( http . StatusBadRequest , gin . H { "error" : err . Error ( ) } )
2021-03-17 10:33:06 +00:00
return
}
2021-03-17 18:52:36 +00:00
l . Tracef ( "parsed form: %+v" , form )
2021-03-18 22:27:43 +00:00
userid , err := a . ValidatePassword ( form . Email , form . Password )
2021-03-17 18:52:36 +00:00
if err != nil {
c . String ( http . StatusForbidden , err . Error ( ) )
2021-03-18 22:27:43 +00:00
return
2021-03-17 18:52:36 +00:00
}
s . Set ( "username" , userid )
2021-03-17 15:01:31 +00:00
if err := s . Save ( ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
return
}
2021-03-18 22:27:43 +00:00
2021-03-17 18:52:36 +00:00
l . Trace ( "redirecting to auth page" )
2021-03-20 22:30:19 +00:00
c . Redirect ( http . StatusFound , oauthAuthorizePath )
2021-03-17 10:33:06 +00:00
}
2021-03-19 23:18:44 +00:00
// TokenPOSTHandler should be served as a POST at https://example.org/oauth/token
2021-03-17 10:33:06 +00:00
// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs.
// See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token
2021-03-19 23:18:44 +00:00
func ( a * API ) TokenPOSTHandler ( c * gin . Context ) {
2021-03-20 22:30:19 +00:00
l := a . log . WithField ( "func" , "TokenPOSTHandler" )
l . Trace ( "entered TokenPOSTHandler" )
// The commented-out code below doesn't work yet because the oauth2 library can't handle OOB properly!
// // make sure redirect_uri is actually set first (we don't accept empty)
// if v, ok := c.GetPostForm("redirect_uri"); !ok || v == "" {
// c.JSON(http.StatusBadRequest, gin.H{"error": "session missing redirect_uri"})
// return
// } else if v == outOfBandRedirect {
// // If redirect_uri is set to out of band, redirect to this endpoint, where we can display the code later
// // This is a bit of a workaround because the oauth library doesn't recognise oob redirect URIs
// c.Request.Form.Set("redirect_uri", fmt.Sprintf("%s://%s%s", a.config.Protocol, a.config.Host, oauthTokenPath))
// }
2021-03-17 15:01:31 +00:00
if err := a . server . HandleTokenRequest ( c . Writer , c . Request ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
2021-03-17 10:33:06 +00:00
}
}
2021-03-18 22:27:43 +00:00
// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
2021-03-17 10:33:06 +00:00
// The idea here is to present an oauth authorize page to the user, with a button
// that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user
2021-03-17 18:52:36 +00:00
func ( a * API ) AuthorizeGETHandler ( c * gin . Context ) {
2021-03-18 22:27:43 +00:00
l := a . log . WithField ( "func" , "AuthorizeGETHandler" )
2021-03-17 15:01:31 +00:00
s := sessions . Default ( c )
2021-03-20 18:06:28 +00:00
// Username will be set in the session by AuthorizePOSTHandler if the caller has already gone through the authentication flow
// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page.
2021-03-17 15:01:31 +00:00
v := s . Get ( "username" )
if username , ok := v . ( string ) ; ! ok || username == "" {
2021-03-18 22:27:43 +00:00
l . Trace ( "username was empty, parsing form then redirecting to sign in page" )
2021-03-20 18:06:28 +00:00
// first make sure they've filled out the authorize form with the required values
2021-03-18 22:27:43 +00:00
form := & mastotypes . OAuthAuthorize { }
if err := c . ShouldBind ( form ) ; err != nil {
c . JSON ( http . StatusBadRequest , gin . H { "error" : err . Error ( ) } )
return
}
l . Tracef ( "parsed form: %+v" , form )
2021-03-20 18:06:28 +00:00
// these fields are *required* so check 'em
2021-03-18 22:27:43 +00:00
if form . ResponseType == "" || form . ClientID == "" || form . RedirectURI == "" {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "missing one of: response_type, client_id or redirect_uri" } )
return
}
// save these values from the form so we can use them elsewhere in the session
s . Set ( "force_login" , form . ForceLogin )
s . Set ( "response_type" , form . ResponseType )
s . Set ( "client_id" , form . ClientID )
s . Set ( "redirect_uri" , form . RedirectURI )
s . Set ( "scope" , form . Scope )
if err := s . Save ( ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
return
}
2021-03-20 18:06:28 +00:00
// send them to the sign in page so we can tell who they are
2021-03-20 22:30:19 +00:00
c . Redirect ( http . StatusFound , authSignInPath )
2021-03-17 15:01:31 +00:00
return
}
2021-03-20 18:06:28 +00:00
// Check if we have a code already. If we do, it means the user used urn:ietf:wg:oauth:2.0:oob as their redirect URI
// and were sent here, which means they just want the code displayed so they can use it out of band.
2021-03-19 23:18:44 +00:00
code := & code { }
2021-03-20 18:06:28 +00:00
if err := c . Bind ( code ) ; err != nil {
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
2021-03-19 23:18:44 +00:00
return
}
2021-03-20 18:06:28 +00:00
// the authorize template will either:
// 1. Display the code to the user if they're already authorized and were redirected here because they selected urn:ietf:wg:oauth:2.0:oob.
// 2. Display a form where they can get some information about the app that's trying to authorize, and approve it, which will then go to AuthorizePOSTHandler
l . Trace ( "serving authorize html" )
c . HTML ( http . StatusOK , "authorize.tmpl" , gin . H {
"code" : code . Code ,
} )
2021-03-17 10:33:06 +00:00
}
2021-03-18 22:27:43 +00:00
// AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize
// The idea here is to present an oauth authorize page to the user, with a button
// that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user
func ( a * API ) AuthorizePOSTHandler ( c * gin . Context ) {
l := a . log . WithField ( "func" , "AuthorizePOSTHandler" )
2021-03-17 15:01:31 +00:00
s := sessions . Default ( c )
2021-03-18 22:27:43 +00:00
v := s . Get ( "username" )
if username , ok := v . ( string ) ; ! ok || username == "" {
c . JSON ( http . StatusUnauthorized , gin . H { "error" : "you are not signed in" } )
}
2021-03-17 15:01:31 +00:00
values := url . Values { }
if v , ok := s . Get ( "force_login" ) . ( string ) ; ! ok {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "session missing force_login" } )
return
} else {
values . Add ( "force_login" , v )
}
if v , ok := s . Get ( "response_type" ) . ( string ) ; ! ok {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "session missing response_type" } )
return
} else {
values . Add ( "response_type" , v )
}
if v , ok := s . Get ( "client_id" ) . ( string ) ; ! ok {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "session missing client_id" } )
return
} else {
values . Add ( "client_id" , v )
}
if v , ok := s . Get ( "redirect_uri" ) . ( string ) ; ! ok {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "session missing redirect_uri" } )
return
} else {
2021-03-20 22:30:19 +00:00
// The commented-out code below doesn't work yet because the oauth2 library can't handle OOB properly!
// if the client requests this particular redirect URI, it means they want to be able to authenticate out of band,
// ie., just have their access_code shown to them so they can do what they want with it later.
//
// But we can't just show the code yet; there's still an authorization flow to go through.
// What we can do is set the redirect uri to the /oauth/authorize page, do the auth
// flow as normal, and then handle showing the code there. See AuthorizeGETHandler.
// if v == outOfBandRedirect {
// v = fmt.Sprintf("%s://%s%s", a.config.Protocol, a.config.Host, oauthAuthorizePath)
// }
2021-03-17 15:01:31 +00:00
values . Add ( "redirect_uri" , v )
}
if v , ok := s . Get ( "scope" ) . ( string ) ; ! ok {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "session missing scope" } )
2021-03-17 10:33:06 +00:00
return
2021-03-17 15:01:31 +00:00
} else {
values . Add ( "scope" , v )
2021-03-17 10:33:06 +00:00
}
2021-03-17 15:01:31 +00:00
if v , ok := s . Get ( "username" ) . ( string ) ; ! ok {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "session missing username" } )
return
} else {
values . Add ( "username" , v )
2021-03-17 10:33:06 +00:00
}
2021-03-17 15:01:31 +00:00
c . Request . Form = values
2021-03-17 18:52:36 +00:00
l . Tracef ( "values on request set to %+v" , c . Request . Form )
2021-03-17 10:33:06 +00:00
2021-03-17 15:01:31 +00:00
if err := s . Save ( ) ; err != nil {
2021-03-17 18:52:36 +00:00
c . JSON ( http . StatusInternalServerError , gin . H { "error" : err . Error ( ) } )
2021-03-17 15:01:31 +00:00
return
}
if err := a . server . HandleAuthorizeRequest ( c . Writer , c . Request ) ; err != nil {
c . JSON ( http . StatusBadRequest , gin . H { "error" : err . Error ( ) } )
2021-03-17 10:33:06 +00:00
}
}
/ *
2021-03-17 12:14:52 +00:00
SUB - HANDLERS -- don ' t serve these directly , they should be attached to the oauth2 server
2021-03-17 10:33:06 +00:00
* /
// PasswordAuthorizationHandler takes a username (in this case, we use an email address)
// and a password. The goal is to authenticate the password against the one for that email
// address stored in the database. If OK, we return the userid (a uuid) for that user,
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
2021-03-17 18:52:36 +00:00
func ( a * API ) ValidatePassword ( email string , password string ) ( userid string , err error ) {
l := a . log . WithField ( "func" , "PasswordAuthorizationHandler" )
2021-03-18 22:27:43 +00:00
// make sure an email/password was provided and bail if not
if email == "" || password == "" {
l . Debug ( "email or password was not provided" )
return incorrectPassword ( )
}
2021-03-15 22:05:24 +00:00
// first we select the user from the database based on email address, bail if no user found for that email
gtsUser := & gtsmodel . User { }
if err := a . conn . Model ( gtsUser ) . Where ( "email = ?" , email ) . Select ( ) ; err != nil {
2021-03-17 18:52:36 +00:00
l . Debugf ( "user %s was not retrievable from db during oauth authorization attempt: %s" , email , err )
2021-03-15 22:05:24 +00:00
return incorrectPassword ( )
}
// make sure a password is actually set and bail if not
if gtsUser . EncryptedPassword == "" {
2021-03-17 18:52:36 +00:00
l . Warnf ( "encrypted password for user %s was empty for some reason" , gtsUser . Email )
2021-03-15 22:05:24 +00:00
return incorrectPassword ( )
}
// compare the provided password with the encrypted one from the db, bail if they don't match
if err := bcrypt . CompareHashAndPassword ( [ ] byte ( gtsUser . EncryptedPassword ) , [ ] byte ( password ) ) ; err != nil {
2021-03-17 18:52:36 +00:00
l . Debugf ( "password hash didn't match for user %s during login attempt: %s" , gtsUser . Email , err )
2021-03-15 22:05:24 +00:00
return incorrectPassword ( )
}
2021-03-18 22:27:43 +00:00
// If we've made it this far the email/password is correct, so we can just return the id of the user.
2021-03-15 22:05:24 +00:00
userid = gtsUser . ID
2021-03-17 18:52:36 +00:00
l . Tracef ( "returning (%s, %s)" , userid , err )
2021-03-15 22:05:24 +00:00
return
}
2021-03-17 10:33:06 +00:00
2021-03-20 18:06:28 +00:00
// UserAuthorizationHandler gets the user's ID from the 'username' field of the request form,
2021-03-17 10:33:06 +00:00
// or redirects to the /auth/sign_in page, if this key is not present.
2021-03-20 18:06:28 +00:00
func ( a * API ) UserAuthorizationHandler ( w http . ResponseWriter , r * http . Request ) ( userID string , err error ) {
2021-03-17 18:52:36 +00:00
l := a . log . WithField ( "func" , "UserAuthorizationHandler" )
2021-03-20 18:06:28 +00:00
userID = r . FormValue ( "username" )
if userID == "" {
2021-03-17 18:52:36 +00:00
l . Trace ( "username was empty, redirecting to sign in page" )
2021-03-20 22:30:19 +00:00
http . Redirect ( w , r , authSignInPath , http . StatusFound )
2021-03-17 15:01:31 +00:00
return "" , nil
2021-03-17 10:33:06 +00:00
}
2021-03-20 18:06:28 +00:00
l . Tracef ( "returning (%s, %s)" , userID , err )
return userID , err
2021-03-17 10:33:06 +00:00
}