2020-10-13 19:35:39 +00:00
package main
import (
"crypto/sha1"
"database/sql"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/spf13/cast"
)
// https://www.w3.org/TR/indieauth/
2020-12-09 16:25:09 +00:00
// https://indieauth.spec.indieweb.org/
2020-10-13 19:35:39 +00:00
type indieAuthData struct {
2020-12-09 16:25:09 +00:00
ClientID string
RedirectURI string
State string
Scopes [ ] string
code string
token string
time time . Time
2020-10-13 19:35:39 +00:00
}
2020-12-09 16:25:09 +00:00
func indieAuthRequest ( w http . ResponseWriter , r * http . Request ) {
// Authorization request
2020-11-06 17:45:31 +00:00
r . ParseForm ( )
data := & indieAuthData {
2020-12-09 16:25:09 +00:00
ClientID : r . Form . Get ( "client_id" ) ,
RedirectURI : r . Form . Get ( "redirect_uri" ) ,
State : r . Form . Get ( "state" ) ,
2020-11-06 17:45:31 +00:00
}
2020-12-09 16:25:09 +00:00
if rt := r . Form . Get ( "response_type" ) ; rt != "code" && rt != "id" && rt != "" {
http . Error ( w , "response_type must be code" , http . StatusBadRequest )
return
2020-11-06 17:45:31 +00:00
}
if scope := r . Form . Get ( "scope" ) ; scope != "" {
data . Scopes = strings . Split ( scope , " " )
}
2020-12-09 16:25:09 +00:00
if ! isValidProfileURL ( data . ClientID ) || ! isValidProfileURL ( data . RedirectURI ) {
http . Error ( w , "client_id and redirect_uri need to by valid URLs" , http . StatusBadRequest )
2020-11-06 17:45:31 +00:00
return
}
if data . State == "" {
http . Error ( w , "state must not be empty" , http . StatusBadRequest )
return
}
render ( w , "indieauth" , & renderData {
Data : data ,
} )
}
2020-10-13 19:35:39 +00:00
func isValidProfileURL ( profileURL string ) bool {
u , err := url . Parse ( profileURL )
if err != nil {
return false
}
if u . Scheme != "http" && u . Scheme != "https" {
return false
}
if u . Fragment != "" {
return false
}
if u . User . String ( ) != "" {
return false
}
if u . Port ( ) != "" {
return false
}
// Missing: Check domain / ip
return true
}
func indieAuthAccept ( w http . ResponseWriter , r * http . Request ) {
// Authentication flow
r . ParseForm ( )
data := & indieAuthData {
2020-12-09 16:25:09 +00:00
ClientID : r . Form . Get ( "client_id" ) ,
RedirectURI : r . Form . Get ( "redirect_uri" ) ,
State : r . Form . Get ( "state" ) ,
Scopes : r . Form [ "scopes" ] ,
time : time . Now ( ) ,
2020-10-13 19:35:39 +00:00
}
sha := sha1 . New ( )
2020-12-09 16:25:09 +00:00
sha . Write ( [ ] byte ( data . time . String ( ) + data . ClientID ) )
2020-10-13 19:35:39 +00:00
data . code = fmt . Sprintf ( "%x" , sha . Sum ( nil ) )
err := data . saveAuthorization ( )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
http . Redirect ( w , r , data . RedirectURI + "?code=" + data . code + "&state=" + data . State , http . StatusFound )
}
type tokenResponse struct {
AccessToken string ` json:"access_token,omitempty" `
TokenType string ` json:"token_type,omitempty" `
Scope string ` json:"scope,omitempty" `
Me string ` json:"me,omitempty" `
ClientID string ` json:"client_id,omitempty" `
}
2020-12-09 16:25:09 +00:00
func indieAuthVerification ( w http . ResponseWriter , r * http . Request ) {
// Authorization verification
r . ParseForm ( )
data := & indieAuthData {
code : r . Form . Get ( "code" ) ,
ClientID : r . Form . Get ( "client_id" ) ,
RedirectURI : r . Form . Get ( "redirect_uri" ) ,
}
valid , err := data . verifyAuthorization ( )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
if ! valid {
http . Error ( w , "Authentication not valid" , http . StatusForbidden )
return
}
res := & tokenResponse {
Me : appConfig . Server . PublicAddress ,
}
w . Header ( ) . Add ( contentType , contentTypeJSONUTF8 )
err = json . NewEncoder ( w ) . Encode ( res )
if err != nil {
w . Header ( ) . Del ( contentType )
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
}
2020-10-13 19:35:39 +00:00
func indieAuthToken ( w http . ResponseWriter , r * http . Request ) {
if r . Method == http . MethodGet {
// Token verification
data , err := verifyIndieAuthToken ( r . Header . Get ( "Authorization" ) )
if err != nil {
http . Error ( w , "Invalid token or token not found" , http . StatusUnauthorized )
2020-11-11 08:03:20 +00:00
return
2020-10-13 19:35:39 +00:00
}
res := & tokenResponse {
Scope : strings . Join ( data . Scopes , " " ) ,
2020-12-09 16:25:09 +00:00
Me : appConfig . Server . PublicAddress ,
2020-10-13 19:35:39 +00:00
ClientID : data . ClientID ,
}
w . Header ( ) . Add ( contentType , contentTypeJSONUTF8 )
err = json . NewEncoder ( w ) . Encode ( res )
if err != nil {
w . Header ( ) . Del ( contentType )
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
2020-11-11 08:03:20 +00:00
return
2020-10-13 19:35:39 +00:00
} else if r . Method == http . MethodPost {
r . ParseForm ( )
// Token Revocation
if r . Form . Get ( "action" ) == "revoke" {
revokeIndieAuthToken ( r . Form . Get ( "token" ) )
w . WriteHeader ( http . StatusOK )
return
}
// Token request
if r . Form . Get ( "grant_type" ) == "authorization_code" {
data := & indieAuthData {
code : r . Form . Get ( "code" ) ,
ClientID : r . Form . Get ( "client_id" ) ,
RedirectURI : r . Form . Get ( "redirect_uri" ) ,
}
2020-12-09 16:25:09 +00:00
valid , err := data . verifyAuthorization ( )
2020-10-13 19:35:39 +00:00
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
if ! valid {
http . Error ( w , "Authentication not valid" , http . StatusForbidden )
return
}
if len ( data . Scopes ) < 1 {
http . Error ( w , "No scope" , http . StatusBadRequest )
return
}
data . time = time . Now ( )
sha := sha1 . New ( )
2020-12-09 16:25:09 +00:00
sha . Write ( [ ] byte ( data . time . String ( ) + data . ClientID ) )
2020-10-13 19:35:39 +00:00
data . token = fmt . Sprintf ( "%x" , sha . Sum ( nil ) )
err = data . saveToken ( )
if err != nil {
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
res := & tokenResponse {
TokenType : "Bearer" ,
AccessToken : data . token ,
Scope : strings . Join ( data . Scopes , " " ) ,
2020-12-09 16:25:09 +00:00
Me : appConfig . Server . PublicAddress ,
2020-10-13 19:35:39 +00:00
}
w . Header ( ) . Add ( contentType , contentTypeJSONUTF8 )
err = json . NewEncoder ( w ) . Encode ( res )
if err != nil {
w . Header ( ) . Del ( contentType )
http . Error ( w , err . Error ( ) , http . StatusInternalServerError )
return
}
2020-11-11 08:03:20 +00:00
return
2020-10-13 19:35:39 +00:00
}
2020-11-11 08:03:20 +00:00
http . Error ( w , "" , http . StatusBadRequest )
return
2020-10-13 19:35:39 +00:00
}
}
func ( data * indieAuthData ) saveAuthorization ( ) ( err error ) {
2020-12-09 16:25:09 +00:00
_ , err = appDbExec ( "insert into indieauthauth (time, code, client, redirect, scope) values (?, ?, ?, ?, ?)" , data . time . Unix ( ) , data . code , data . ClientID , data . RedirectURI , strings . Join ( data . Scopes , " " ) )
2020-10-13 19:35:39 +00:00
return
}
2020-12-09 16:25:09 +00:00
func ( data * indieAuthData ) verifyAuthorization ( ) ( valid bool , err error ) {
2020-10-13 19:35:39 +00:00
// code valid for 600 seconds
2020-12-09 16:25:09 +00:00
row , err := appDbQueryRow ( "select code, client, redirect, scope from indieauthauth where time >= ? and code = ? and client = ? and redirect = ?" , time . Now ( ) . Unix ( ) - 600 , data . code , data . ClientID , data . RedirectURI )
if err != nil {
return false , err
}
scope := ""
err = row . Scan ( & data . code , & data . ClientID , & data . RedirectURI , & scope )
if err == sql . ErrNoRows {
return false , nil
} else if err != nil {
return false , err
}
if scope != "" {
data . Scopes = strings . Split ( scope , " " )
2020-10-13 19:35:39 +00:00
}
valid = true
2020-11-09 15:40:12 +00:00
_ , err = appDbExec ( "delete from indieauthauth where code = ? or time < ?" , data . code , time . Now ( ) . Unix ( ) - 600 )
2020-10-13 19:35:39 +00:00
data . code = ""
return
}
func ( data * indieAuthData ) saveToken ( ) ( err error ) {
2020-12-09 16:25:09 +00:00
_ , err = appDbExec ( "insert into indieauthtoken (time, token, client, scope) values (?, ?, ?, ?)" , data . time . Unix ( ) , data . token , data . ClientID , strings . Join ( data . Scopes , " " ) )
2020-10-13 19:35:39 +00:00
return
}
func verifyIndieAuthToken ( token string ) ( data * indieAuthData , err error ) {
token = strings . ReplaceAll ( token , "Bearer " , "" )
2020-11-10 06:45:32 +00:00
data = & indieAuthData {
Scopes : [ ] string { } ,
}
2020-12-09 16:25:09 +00:00
row , err := appDbQueryRow ( "select time, token, client, scope from indieauthtoken where token = @token" , sql . Named ( "token" , token ) )
2020-11-09 15:40:12 +00:00
if err != nil {
return nil , err
}
2020-10-13 19:35:39 +00:00
timeString := ""
scope := ""
2020-12-09 16:25:09 +00:00
err = row . Scan ( & timeString , & data . token , & data . ClientID , & scope )
2020-10-13 19:35:39 +00:00
if err == sql . ErrNoRows {
return nil , errors . New ( "token not found" )
} else if err != nil {
return nil , err
}
if scope != "" {
data . Scopes = strings . Split ( scope , " " )
}
data . time = time . Unix ( cast . ToInt64 ( timeString ) , 0 )
return
}
func revokeIndieAuthToken ( token string ) {
if token == "" {
return
}
2020-11-09 15:40:12 +00:00
_ , _ = appDbExec ( "delete from indieauthtoken where token=?" , token )
2020-10-13 19:35:39 +00:00
return
}