checkpoint: add login with Oauth2
This commit is contained in:
parent
804268f298
commit
50d2f5f8d2
4 changed files with 193 additions and 30 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/environment/
|
11
go.mod
11
go.mod
|
@ -1,3 +1,12 @@
|
||||||
module enbas
|
module codeflow.dananglin.me.uk/apollo/enbas
|
||||||
|
|
||||||
go 1.22.0
|
go 1.22.0
|
||||||
|
|
||||||
|
require golang.org/x/oauth2 v0.17.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
golang.org/x/net v0.21.0 // indirect
|
||||||
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
|
)
|
||||||
|
|
24
go.sum
Normal file
24
go.sum
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
|
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||||
|
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||||
|
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||||
|
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||||
|
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
185
main.go
185
main.go
|
@ -4,26 +4,31 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
"golang.org/x/oauth2"
|
||||||
register()
|
)
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
applicationName = "Enbas"
|
applicationName = "enbas"
|
||||||
redirectUris = "urn:ietf:wg:oauth:2.0:oob"
|
applicationWebsite = "https://codeflow.dananglin.me.uk/apollo/enbas"
|
||||||
|
redirectUri = "urn:ietf:wg:oauth:2.0:oob"
|
||||||
|
userAgent = "Enbas/0.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AccountConfig struct {
|
type AuthenticationConfig struct {
|
||||||
CurrentAccount string `json:"currentAccount"`
|
CurrentAccount string `json:"currentAccount"`
|
||||||
Accounts map[string]Account `json:"accounts"`
|
Authentications map[string]Authentication `json:"authentications"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Authentication struct {
|
||||||
Instance string `json:"instance"`
|
Instance string `json:"instance"`
|
||||||
ClientID string `json:"clientId"`
|
ClientID string `json:"clientId"`
|
||||||
ClientSecret string `json:"clientSecret"`
|
ClientSecret string `json:"clientSecret"`
|
||||||
|
@ -47,25 +52,86 @@ type RegisterResponse struct {
|
||||||
Website string `json:"website"`
|
Website string `json:"website"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func register() Account {
|
var errEmptyAccessToken = errors.New("received an empty access token")
|
||||||
// ask user for instance, if not start with http(s), prepend with https.
|
|
||||||
|
var consentMessageFormat = `
|
||||||
|
You'll need to sign into your GoToSocial's consent page in order to generate the out-of-band token to continue with
|
||||||
|
the application's login process. Your browser may have opened the link to the consent page already. If not, please
|
||||||
|
copy and paste the link below to your browser:
|
||||||
|
|
||||||
|
%s
|
||||||
|
|
||||||
|
Once you have the code please copy and paste it below.
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := loginWithOauth2(); err != nil {
|
||||||
|
fmt.Printf("ERROR: Unable to register the application; %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginWithOauth2() error {
|
||||||
|
var instance string
|
||||||
|
|
||||||
fmt.Print("Please enter the instance URL: ")
|
fmt.Print("Please enter the instance URL: ")
|
||||||
|
|
||||||
instance := ""
|
|
||||||
if _, err := fmt.Scanln(&instance); err != nil {
|
if _, err := fmt.Scanln(&instance); err != nil {
|
||||||
panic(err)
|
return fmt.Errorf("unable to read user input for the instance value; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication, err := register(instance)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to register the application; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
consentPageURL := authCodeURL(authentication)
|
||||||
|
|
||||||
|
openLink(consentPageURL)
|
||||||
|
|
||||||
|
fmt.Printf(consentMessageFormat, consentPageURL)
|
||||||
|
|
||||||
|
var code string
|
||||||
|
fmt.Print("Out-of-band token: ")
|
||||||
|
|
||||||
|
if _, err := fmt.Scanln(&code); err != nil {
|
||||||
|
return fmt.Errorf("failed to read access code; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication, err = addAccessToken(authentication, code)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get the access token; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%+v", authentication)
|
||||||
|
|
||||||
|
// validate authentication and get username for file save
|
||||||
|
|
||||||
|
// save the authentication to a file
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func register(instance string) (Authentication, error) {
|
||||||
|
if !strings.HasPrefix(instance, "https") || !strings.HasPrefix(instance, "http") {
|
||||||
|
instance = "https://" + instance
|
||||||
|
}
|
||||||
|
|
||||||
|
for strings.HasSuffix(instance, "/") {
|
||||||
|
instance = instance[:len(instance)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
request := RegisterRequest{
|
request := RegisterRequest{
|
||||||
ClientName: applicationName,
|
ClientName: applicationName,
|
||||||
RedirectUris: redirectUris,
|
RedirectUris: redirectUri,
|
||||||
Scopes: "read",
|
Scopes: "read write",
|
||||||
Website: "",
|
Website: applicationWebsite,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(request)
|
data, err := json.Marshal(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return Authentication{}, fmt.Errorf("unable to marshal the request body; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpRequestBody := bytes.NewBuffer(data)
|
httpRequestBody := bytes.NewBuffer(data)
|
||||||
|
@ -73,22 +139,29 @@ func register() Account {
|
||||||
path := "/api/v1/apps"
|
path := "/api/v1/apps"
|
||||||
url := instance + path
|
url := instance + path
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, url, httpRequestBody)
|
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, url, httpRequestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return Authentication{}, fmt.Errorf("unable to create the HTTP request; %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpRequest.Header.Add("Content-Type", "application/json")
|
httpRequest.Header.Add("Content-Type", "application/json")
|
||||||
httpRequest.Header.Set("User-Agent", "Enbas/0.0.0")
|
httpRequest.Header.Set("User-Agent", userAgent)
|
||||||
|
|
||||||
httpClient := http.Client{}
|
httpClient := http.Client{}
|
||||||
|
|
||||||
httpResponse, err := httpClient.Do(httpRequest)
|
httpResponse, err := httpClient.Do(httpRequest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return Authentication{}, fmt.Errorf("received an error after sending the request; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if httpResponse.StatusCode != http.StatusOK {
|
||||||
|
return Authentication{}, fmt.Errorf(
|
||||||
|
"did not receive an OK response from the GoToSocial server; got %d",
|
||||||
|
httpResponse.StatusCode,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer httpResponse.Body.Close()
|
defer httpResponse.Body.Close()
|
||||||
|
@ -96,19 +169,75 @@ func register() Account {
|
||||||
var registerResponse RegisterResponse
|
var registerResponse RegisterResponse
|
||||||
|
|
||||||
if err := json.NewDecoder(httpResponse.Body).Decode(®isterResponse); err != nil {
|
if err := json.NewDecoder(httpResponse.Body).Decode(®isterResponse); err != nil {
|
||||||
panic(err)
|
return Authentication{}, fmt.Errorf(
|
||||||
|
"unable to decode the response from the GoToSocial server; %w",
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(registerResponse)
|
account := Authentication{
|
||||||
|
|
||||||
fmt.Println("Please sign into your instance with your browser and enter the access token\nAccess token: ")
|
|
||||||
|
|
||||||
account := Account{
|
|
||||||
Instance: instance,
|
Instance: instance,
|
||||||
ClientID: registerResponse.ClientID,
|
ClientID: registerResponse.ClientID,
|
||||||
ClientSecret: registerResponse.ClientSecret,
|
ClientSecret: registerResponse.ClientSecret,
|
||||||
AccessToken: "",
|
AccessToken: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
return account
|
return account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func authCodeURL(account Authentication) string {
|
||||||
|
config := oauth2.Config{
|
||||||
|
ClientID: account.ClientID,
|
||||||
|
ClientSecret: account.ClientSecret,
|
||||||
|
Scopes: []string{"read", "write"},
|
||||||
|
RedirectURL: redirectUri,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: account.Instance + "/oauth/authorize",
|
||||||
|
TokenURL: account.Instance + "/oauth/token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
url := config.AuthCodeURL("state", oauth2.AccessTypeOffline) + fmt.Sprintf("&client_name=%s", applicationName)
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func addAccessToken(authentication Authentication, code string) (Authentication, error) {
|
||||||
|
config := oauth2.Config{
|
||||||
|
ClientID: authentication.ClientID,
|
||||||
|
ClientSecret: authentication.ClientSecret,
|
||||||
|
Scopes: []string{"read", "write"},
|
||||||
|
RedirectURL: redirectUri,
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: authentication.Instance + "/oauth/authorize",
|
||||||
|
TokenURL: authentication.Instance + "/oauth/token",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := config.Exchange(context.Background(), code)
|
||||||
|
if err != nil {
|
||||||
|
return Authentication{}, fmt.Errorf("unable to exchange the code for an access token; %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if token == nil || token.AccessToken == "" {
|
||||||
|
return Authentication{}, errEmptyAccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication.AccessToken = token.AccessToken
|
||||||
|
|
||||||
|
return authentication, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openLink(url string) {
|
||||||
|
var open string
|
||||||
|
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
open = "xdg-open"
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
command := exec.Command(open, url)
|
||||||
|
|
||||||
|
command.Start()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue