2020-10-13 19:35:39 +00:00
package main
import (
"crypto/sha1"
"database/sql"
2021-01-21 16:59:47 +00:00
"encoding/json"
2020-10-13 19:35:39 +00:00
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/spf13/cast"
2021-06-28 20:17:18 +00:00
"go.goblog.app/app/pkgs/contenttype"
2020-10-13 19:35:39 +00:00
)
// 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
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) indieAuthRequest ( w http . ResponseWriter , r * http . Request ) {
2020-12-09 16:25:09 +00:00
// Authorization request
2021-02-08 17:51:07 +00:00
if err := r . ParseForm ( ) ; err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusBadRequest )
2021-02-08 17:51:07 +00:00
return
}
2020-11-06 17:45:31 +00:00
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 != "" {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "response_type must be code" , http . StatusBadRequest )
2020-12-09 16:25:09 +00:00
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 ) {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "client_id and redirect_uri need to by valid URLs" , http . StatusBadRequest )
2020-11-06 17:45:31 +00:00
return
}
if data . State == "" {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "state must not be empty" , http . StatusBadRequest )
2020-11-06 17:45:31 +00:00
return
}
2021-06-06 12:39:42 +00:00
a . render ( w , r , "indieauth" , & renderData {
2020-11-06 17:45:31 +00:00
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
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) indieAuthAccept ( w http . ResponseWriter , r * http . Request ) {
2020-10-13 19:35:39 +00:00
// Authentication flow
2021-02-08 17:51:07 +00:00
if err := r . ParseForm ( ) ; err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusBadRequest )
2021-02-08 17:51:07 +00:00
return
}
2020-10-13 19:35:39 +00:00
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" ] ,
2021-07-13 15:23:10 +00:00
time : time . Now ( ) . UTC ( ) ,
2020-10-13 19:35:39 +00:00
}
sha := sha1 . New ( )
2021-07-13 15:23:10 +00:00
if _ , err := sha . Write ( [ ] byte ( data . time . Format ( time . RFC3339 ) + data . ClientID ) ) ; err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusInternalServerError )
2021-02-08 17:51:07 +00:00
return
}
2020-10-13 19:35:39 +00:00
data . code = fmt . Sprintf ( "%x" , sha . Sum ( nil ) )
2021-06-06 12:39:42 +00:00
err := a . db . saveAuthorization ( data )
2020-10-13 19:35:39 +00:00
if err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusInternalServerError )
2020-10-13 19:35:39 +00:00
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" `
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) indieAuthVerification ( w http . ResponseWriter , r * http . Request ) {
2020-12-09 16:25:09 +00:00
// Authorization verification
2021-02-08 17:51:07 +00:00
if err := r . ParseForm ( ) ; err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusBadRequest )
2021-02-08 17:51:07 +00:00
return
}
2020-12-09 16:25:09 +00:00
data := & indieAuthData {
code : r . Form . Get ( "code" ) ,
ClientID : r . Form . Get ( "client_id" ) ,
RedirectURI : r . Form . Get ( "redirect_uri" ) ,
}
2021-06-06 12:39:42 +00:00
valid , err := a . db . verifyAuthorization ( data )
2020-12-09 16:25:09 +00:00
if err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusInternalServerError )
2020-12-09 16:25:09 +00:00
return
}
if ! valid {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "Authentication not valid" , http . StatusForbidden )
2020-12-09 16:25:09 +00:00
return
}
2021-02-16 15:26:21 +00:00
b , _ := json . Marshal ( tokenResponse {
2021-06-06 12:39:42 +00:00
Me : a . cfg . Server . PublicAddress ,
2021-02-16 15:26:21 +00:00
} )
2021-06-18 12:32:03 +00:00
w . Header ( ) . Set ( contentType , contenttype . JSONUTF8 )
_ , _ = a . min . Write ( w , contenttype . JSON , b )
2020-12-09 16:25:09 +00:00
}
2021-06-06 12:39:42 +00:00
func ( a * goBlog ) indieAuthToken ( w http . ResponseWriter , r * http . Request ) {
2020-10-13 19:35:39 +00:00
if r . Method == http . MethodGet {
// Token verification
2021-06-06 12:39:42 +00:00
data , err := a . db . verifyIndieAuthToken ( r . Header . Get ( "Authorization" ) )
2020-10-13 19:35:39 +00:00
if err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "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 , " " ) ,
2021-06-06 12:39:42 +00:00
Me : a . cfg . Server . PublicAddress ,
2020-10-13 19:35:39 +00:00
ClientID : data . ClientID ,
}
2021-02-16 15:26:21 +00:00
b , _ := json . Marshal ( res )
2021-06-18 12:32:03 +00:00
w . Header ( ) . Set ( contentType , contenttype . JSONUTF8 )
_ , _ = a . min . Write ( w , contenttype . JSON , b )
2020-11-11 08:03:20 +00:00
return
2020-10-13 19:35:39 +00:00
} else if r . Method == http . MethodPost {
2021-02-08 17:51:07 +00:00
if err := r . ParseForm ( ) ; err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusBadRequest )
2021-02-08 17:51:07 +00:00
return
}
2020-10-13 19:35:39 +00:00
// Token Revocation
if r . Form . Get ( "action" ) == "revoke" {
2021-06-06 12:39:42 +00:00
a . db . revokeIndieAuthToken ( r . Form . Get ( "token" ) )
2020-10-13 19:35:39 +00:00
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" ) ,
}
2021-06-06 12:39:42 +00:00
valid , err := a . db . verifyAuthorization ( data )
2020-10-13 19:35:39 +00:00
if err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusInternalServerError )
2020-10-13 19:35:39 +00:00
return
}
if ! valid {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "Authentication not valid" , http . StatusForbidden )
2020-10-13 19:35:39 +00:00
return
}
if len ( data . Scopes ) < 1 {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "No scope" , http . StatusBadRequest )
2020-10-13 19:35:39 +00:00
return
}
2021-07-13 15:23:10 +00:00
data . time = time . Now ( ) . UTC ( )
2020-10-13 19:35:39 +00:00
sha := sha1 . New ( )
2021-07-13 15:23:10 +00:00
if _ , err := sha . Write ( [ ] byte ( data . time . Format ( time . RFC3339 ) + data . ClientID ) ) ; err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusInternalServerError )
2021-02-08 17:51:07 +00:00
return
}
2020-10-13 19:35:39 +00:00
data . token = fmt . Sprintf ( "%x" , sha . Sum ( nil ) )
2021-06-06 12:39:42 +00:00
err = a . db . saveToken ( data )
2020-10-13 19:35:39 +00:00
if err != nil {
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , err . Error ( ) , http . StatusInternalServerError )
2020-10-13 19:35:39 +00:00
return
}
res := & tokenResponse {
TokenType : "Bearer" ,
AccessToken : data . token ,
Scope : strings . Join ( data . Scopes , " " ) ,
2021-06-06 12:39:42 +00:00
Me : a . cfg . Server . PublicAddress ,
2020-10-13 19:35:39 +00:00
}
2021-02-16 15:26:21 +00:00
b , _ := json . Marshal ( res )
2021-06-18 12:32:03 +00:00
w . Header ( ) . Set ( contentType , contenttype . JSONUTF8 )
_ , _ = a . min . Write ( w , contenttype . JSON , b )
2020-11-11 08:03:20 +00:00
return
2020-10-13 19:35:39 +00:00
}
2021-06-06 12:39:42 +00:00
a . serveError ( w , r , "" , http . StatusBadRequest )
2020-11-11 08:03:20 +00:00
return
2020-10-13 19:35:39 +00:00
}
}
2021-06-06 12:39:42 +00:00
func ( db * database ) saveAuthorization ( data * indieAuthData ) ( err error ) {
_ , err = db . exec ( "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
}
2021-06-06 12:39:42 +00:00
func ( db * database ) verifyAuthorization ( data * indieAuthData ) ( valid bool , err error ) {
2020-10-13 19:35:39 +00:00
// code valid for 600 seconds
2021-06-06 12:39:42 +00:00
row , err := db . queryRow ( "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 )
2020-12-09 16:25:09 +00:00
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
2021-06-06 12:39:42 +00:00
_ , err = db . exec ( "delete from indieauthauth where code = ? or time < ?" , data . code , time . Now ( ) . Unix ( ) - 600 )
2020-10-13 19:35:39 +00:00
data . code = ""
return
}
2021-06-06 12:39:42 +00:00
func ( db * database ) saveToken ( data * indieAuthData ) ( err error ) {
_ , err = db . exec ( "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
}
2021-06-06 12:39:42 +00:00
func ( db * database ) verifyIndieAuthToken ( token string ) ( data * indieAuthData , err error ) {
2020-10-13 19:35:39 +00:00
token = strings . ReplaceAll ( token , "Bearer " , "" )
2020-11-10 06:45:32 +00:00
data = & indieAuthData {
Scopes : [ ] string { } ,
}
2021-06-06 12:39:42 +00:00
row , err := db . queryRow ( "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
}
2021-06-06 12:39:42 +00:00
func ( db * database ) revokeIndieAuthToken ( token string ) {
2021-02-08 17:51:07 +00:00
if token != "" {
2021-06-06 12:39:42 +00:00
_ , _ = db . exec ( "delete from indieauthtoken where token=?" , token )
2020-10-13 19:35:39 +00:00
}
}