Refactor and added ability to use include multiple providers in config

This commit is contained in:
David Allen 2024-03-03 18:23:35 -07:00
parent 53d1a8cc35
commit 4bca62ec2f
No known key found for this signature in database
GPG key ID: 1D2A29322FBB6FCB
13 changed files with 660 additions and 712 deletions

View file

@ -1,29 +1,73 @@
version: "0.0.1"
server: server:
host: 127.0.0.1 host: "127.0.0.1"
port: 3333 port: 3333
client: callback: "/oidc/callback"
id: 7527e7b4-c96a-4df0-8fc5-00fde18bb65d
secret: gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq providers:
redirect-uris: facebook: "http://facebook.com"
- "http://127.0.0.1:3333/oidc/callback" forgejo: "http://git.towk.local:3000"
oidc: gitlab: "https://gitlab.newmexicoconsortium.org"
issuer: "http://git.towk.local:3000/" github: "https://github.com"
urls:
#identities: http://127.0.0.1:4434/admin/identities authentication:
trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers clients:
access-token: http://127.0.0.1:4444/oauth2/token - id: "1135541217802147"
server-config: http://git.towk.local:3000/.well-known/openid-configuration secret: "b3a3123e8235de1dbab448369bc3d024"
jwks_uri: http://git.towk.local:3000/login/oauth/keys issuer: "https://www.facebook.com"
login: http://127.0.0.1:4433/self-service/login/api scope:
login-flow-id: http://127.0.0.1:4433/self-service/login/flows?id={id} - "openid"
register-client: http://127.0.0.1:4445/clients - "name"
authorize-client: http://127.0.0.1:4444/oauth2/authorize - "email"
state: "" redirect-uris:
response-type: code - "http://127.0.0.1:3333/oidc/callback"
decode-id-token: true - id: "978b48059dd4916f53b4"
decode-access-token: true secret: "eb54b533eb6afd695e3a1b3f363ab2b29acc7425"
run-once: true issuer: "https://github.com"
scope: scope:
- openid - "openid"
- profile - "profile"
- email redirect-uris:
- "http://127.0.0.1:3333/oidc/callback"
- id: "7527e7b4-c96a-4df0-8fc5-00fde18bb65d"
secret: "gto_cc5uvpb5lsdczkwnbarvwmbpv5kcjwg7nhbc75zt65yrfh2ldenq"
name: "forgejo"
issuer: "http://git.towk.local:3000"
scope:
- "openid"
- "profile"
- "read"
- "email"
redirect-uris:
- "http://127.0.0.1:3333/oidc/callback"
- id: "7c0fab1153674a258a705976fcb9468350df3addd91de4ec622fc9ed24bfbcdd"
secret: "a9a8bc55b0cd99236756093adc00ab17855fa507ce106b8038e7f9390ef2ad99"
name: "gitlab"
issuer: "http://gitlab.newmexicoconsortium.org"
scope:
- "openid"
- "profile"
- "email"
redirect-uris:
- "http://127.0.0.1:3333/oidc/callback"
flows:
authorization-code:
state: ""
client-credentials:
authorization:
urls:
#identities: http://127.0.0.1:4434/admin/identities
trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers
login: http://127.0.0.1:4433/self-service/login/api
clients: http://127.0.0.1:4445/admin/clients
authorize: http://127.0.0.1:4444/oauth2/auth
register: http://127.0.0.1:4444/oauth2/register
token: http://127.0.0.1:4444/oauth2/token
options:
decode-id-token: true
decode-access-token: true
run-once: true
open-browser: false

118
internal/authenticate.go Normal file
View file

@ -0,0 +1,118 @@
package opaal
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/davidallendj/go-utils/httpx"
)
func (client *Client) IsFlowInitiated() bool {
return client.FlowId != ""
}
func (client *Client) BuildAuthorizationUrl(issuer string, state string) string {
return issuer + "?" + "client_id=" + client.Id +
"&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) +
"&response_type=code" + // this has to be set to "code"
"&state=" + state +
"&scope=" + strings.Join(client.Scope, "+") +
"&resource=" + url.QueryEscape("http://127.0.0.1:4444/oauth2/token")
}
func (client *Client) InitiateLoginFlow(loginUrl string) error {
// kratos: GET /self-service/login/api
req, err := http.NewRequest("GET", loginUrl, bytes.NewBuffer([]byte{}))
if err != nil {
return fmt.Errorf("failed to make request: %v", err)
}
res, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to do request: %v", err)
}
defer res.Body.Close()
// get the flow ID from response
body, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %v", err)
}
var flowData map[string]any
err = json.Unmarshal(body, &flowData)
if err != nil {
return fmt.Errorf("failed to unmarshal flow data: %v\n%v", err, string(body))
} else {
client.FlowId = flowData["id"].(string)
}
return nil
}
func (client *Client) FetchFlowData(url string) (map[string]any, error) {
//kratos: GET /self-service/login/flows?id={flowId}
// replace {id} in string with actual value
url = strings.ReplaceAll(url, "{id}", client.FlowId)
_, b, err := httpx.MakeHttpRequest(url, http.MethodGet, nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
}
var flowData map[string]any
err = json.Unmarshal(b, &flowData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal flow data: %v", err)
}
return flowData, nil
}
func (client *Client) FetchCSRFToken(flowUrl string) error {
data, err := client.FetchFlowData(flowUrl)
if err != nil {
return fmt.Errorf("failed to fetch flow data: %v", err)
}
// iterate through nodes and extract the CSRF token attribute from the flow data
ui := data["ui"].(map[string]any)
nodes := ui["nodes"].([]any)
for _, node := range nodes {
attrs := node.(map[string]any)["attributes"].(map[string]any)
name := attrs["name"].(string)
if name == "csrf_token" {
client.CsrfToken = attrs["value"].(string)
return nil
}
}
return fmt.Errorf("failed to extract CSRF token: not found")
}
func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) {
body := url.Values{
"grant_type": {"authorization_code"},
"client_id": {client.Id},
"client_secret": {client.Secret},
"redirect_uri": {strings.Join(client.RedirectUris, ",")},
}
// add optional params if valid
if code != "" {
body["code"] = []string{code}
}
if state != "" {
body["state"] = []string{state}
}
res, err := http.PostForm(remoteUrl, body)
if err != nil {
return nil, fmt.Errorf("failed to get ID token: %s", err)
}
defer res.Body.Close()
// domain, _ := url.Parse("http://127.0.0.1")
// client.Jar.SetCookies(domain, res.Cookies())
return io.ReadAll(res.Body)
}

241
internal/authorize.go Normal file
View file

@ -0,0 +1,241 @@
package opaal
import (
"bytes"
"davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"slices"
"strings"
"time"
"github.com/davidallendj/go-utils/httpx"
"github.com/davidallendj/go-utils/util"
)
func (client *Client) AddTrustedIssuer(url string, idp *oidc.IdentityProvider, subject string, duration time.Duration) ([]byte, error) {
// hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers
if idp == nil {
return nil, fmt.Errorf("identity provided is nil")
}
jwkstr, err := json.Marshal(idp.Key)
if err != nil {
return nil, fmt.Errorf("failed to marshal JWK: %v", err)
}
quotedScopes := make([]string, len(client.Scope))
for i, s := range client.Scope {
quotedScopes[i] = fmt.Sprintf("\"%s\"", s)
}
// NOTE: Can also include "jwks_uri" instead
data := []byte(fmt.Sprintf("{"+
"\"allow_any_subject\": false,"+
"\"issuer\": \"%s\","+
"\"subject\": \"%s\","+
"\"expires_at\": \"%v\","+
"\"jwk\": %v,"+
"\"scope\": [ %s ]"+
"}", idp.Issuer, subject, time.Now().Add(duration).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ",")))
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
// req.Header.Add("X-CSRF-Token", client.CsrfToken.Value)
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
}
req.Header.Add("Content-Type", "application/json")
// req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken))
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to do request: %v", err)
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func (client *Client) IsOAuthClientRegistered(clientUrl string) (bool, error) {
_, _, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil)
if err != nil {
return false, fmt.Errorf("failed to make request: %v", err)
}
// TODO: need to check contents of actual response
return true, nil
}
func (client *Client) GetOAuthClient(clientUrl string) error {
_, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodGet, nil, nil)
if err != nil {
return fmt.Errorf("failed to make request: %v", err)
}
fmt.Printf("GetOAuthClient: %v\n", string(b))
var data []map[string]any
err = json.Unmarshal(b, &data)
if err != nil {
return fmt.Errorf("failed to unmarshal JSON: %v", err)
}
index := slices.IndexFunc(data, func(c map[string]any) bool {
if c["client_id"] == nil {
return false
}
return c["client_id"].(string) == client.Id
})
if index < 0 {
return fmt.Errorf("client not found")
}
// cast the redirect_uris from []any to []string and extract registration token
foundClient := data[index]
for _, uri := range foundClient["redirect_uris"].([]any) {
client.RedirectUris = append(client.RedirectUris, uri.(string))
}
if foundClient["registration-access-token"] != nil {
client.RegistrationAccessToken = foundClient["registration-access-token"].(string)
}
return nil
}
func (client *Client) CreateOAuthClient(registerUrl string, audience []string) ([]byte, error) {
// hydra endpoint: POST /clients
audience = util.QuoteArrayStrings(audience)
body := httpx.Body(fmt.Sprintf(`{
"client_id": "%s",
"client_name": "%s",
"client_secret": "%s",
"token_endpoint_auth_method": "client_secret_post",
"scope": "%s",
"grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"],
"response_types": ["token"],
"redirect_uris": ["http://127.0.0.1:3333/callback"],
"state": 12345678910,
"audience": [%s]
}`, client.Id, client.Id, client.Secret, strings.Join(client.Scope, " "), strings.Join(audience, ","),
))
headers := httpx.Headers{
"Content-Type": "application/json",
}
_, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, []byte(body), headers)
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
}
var rjson map[string]any
err = json.Unmarshal(b, &rjson)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response body: %v", err)
}
// check for error first
errJson := rjson["error"]
if errJson == nil {
// set the client ID and secret of registered client
client.Id = rjson["client_id"].(string)
client.Secret = rjson["client_secret"].(string)
client.RegistrationAccessToken = rjson["registration_access_token"].(string)
} else {
return b, nil
}
return b, err
}
func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) {
// hydra endpoint: POST /oauth2/register
audience = util.QuoteArrayStrings(audience)
body := httpx.Body(fmt.Sprintf(`{
"client_name": "opaal",
"token_endpoint_auth_method": "client_secret_post",
"scope": "%s",
"grant_types": ["urn:ietf:params:oauth:grant-type:jwt-bearer"],
"response_types": ["token"],
"redirect_uris": ["http://127.0.0.1:3333/callback"],
"state": 12345678910,
"audience": [%s]
}`, strings.Join(client.Scope, " "), strings.Join(audience, ","),
))
headers := httpx.Headers{
"Content-Type": "application/json",
}
_, b, err := httpx.MakeHttpRequest(registerUrl, http.MethodPost, body, headers)
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
}
var rjson map[string]any
err = json.Unmarshal(b, &rjson)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal response body: %v", err)
}
// check for error first
errJson := rjson["error"]
if errJson == nil {
// set the client ID and secret of registered client
client.Id = rjson["client_id"].(string)
client.Secret = rjson["client_secret"].(string)
client.RegistrationAccessToken = rjson["registration_access_token"].(string)
} else {
return b, nil
}
return b, err
}
func (client *Client) AuthorizeOAuthClient(authorizeUrl string) ([]byte, error) {
// set the authorization header
body := []byte("grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") +
"&scope=" + strings.Join(client.Scope, "+") +
"&client_id=" + client.Id +
"&client_secret=" + client.Secret +
"&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback") + // FIXME: needs to not be hardcorded
"&response_type=token" +
"&state=12345678910",
)
headers := httpx.Headers{
"Authorization": "Bearer " + client.RegistrationAccessToken,
"Content-Type": "application/x-www-form-urlencoded",
}
_, b, err := httpx.MakeHttpRequest(authorizeUrl, http.MethodPost, body, headers)
if err != nil {
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
}
return b, nil
}
func (client *Client) PerformTokenGrant(clientUrl string, jwt string) ([]byte, error) {
// hydra endpoint: /oauth/token
body := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") +
"&client_id=" + client.Id +
"&client_secret=" + client.Secret +
"&redirect_uri=" + url.QueryEscape("http://127.0.0.1:3333/callback")
// add optional params if valid
if jwt != "" {
body += "&assertion=" + jwt
}
if client.Scope != nil || len(client.Scope) > 0 {
body += "&scope=" + strings.Join(client.Scope, "+")
}
headers := httpx.Headers{
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": "Bearer " + client.RegistrationAccessToken,
}
_, b, err := httpx.MakeHttpRequest(clientUrl, http.MethodPost, []byte(body), headers)
// set flow ID back to empty string to indicate a completed flow
client.FlowId = ""
return b, err
}
func (client *Client) DeleteOAuthClient(clientUrl string) error {
_, _, err := httpx.MakeHttpRequest(clientUrl+"/"+client.Id, http.MethodDelete, nil, nil)
if err != nil {
return fmt.Errorf("failed to make request: %v", err)
}
return nil
}

View file

@ -1,316 +1,90 @@
package opaal package opaal
import ( import (
"bytes"
"davidallendj/opaal/internal/oidc"
"encoding/json"
"fmt"
"io"
"net/http" "net/http"
"net/http/cookiejar" "net/http/cookiejar"
"net/url" "slices"
"strings"
"time"
"github.com/davidallendj/go-utils/httpx" "github.com/davidallendj/go-utils/mathx"
"github.com/davidallendj/go-utils/util"
"golang.org/x/net/publicsuffix" "golang.org/x/net/publicsuffix"
) )
type Client struct { type Client struct {
http.Client http.Client
Id string `yaml:"id"` Id string `yaml:"id"`
Secret string `yaml:"secret"` Secret string `yaml:"secret"`
RedirectUris []string `yaml:"redirect-uris"` Name string `yaml:"name"`
FlowId string Description string `yaml:"description"`
CsrfToken string Issuer string `yaml:"issuer"`
RegistrationAccessToken string `yaml:"registration-access-token"`
RedirectUris []string `yaml:"redirect-uris"`
Scope []string `yaml:"scope"`
FlowId string
CsrfToken string
}
func NewClient() *Client {
return &Client{}
} }
func NewClientWithConfig(config *Config) *Client { func NewClientWithConfig(config *Config) *Client {
jar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) // make sure config is valid
if config == nil {
return nil
}
// make sure we have at least one client
clients := config.Authentication.Clients
if len(clients) <= 0 {
return nil
}
// use the first client found by default
return &Client{ return &Client{
Id: config.Client.Id, Id: clients[0].Id,
Secret: config.Client.Secret, Secret: clients[0].Secret,
RedirectUris: config.Client.RedirectUris, Name: clients[0].Name,
Client: http.Client{Jar: jar}, Issuer: clients[0].Issuer,
Scope: clients[0].Scope,
RedirectUris: clients[0].RedirectUris,
} }
} }
func (client *Client) IsFlowInitiated() bool { func NewClientWithConfigByIndex(config *Config, index int) *Client {
return client.FlowId != "" size := len(config.Authentication.Clients)
index = mathx.Clamp(index, 0, size)
return nil
} }
func (client *Client) BuildAuthorizationUrl(authEndpoint string, state string, responseType string, scope []string) string { func NewClientWithConfigByName(config *Config, name string) *Client {
return authEndpoint + "?" + "client_id=" + client.Id + index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
"&redirect_uri=" + url.QueryEscape(strings.Join(client.RedirectUris, ",")) + return c.Name == name
"&response_type=" + responseType + })
"&state=" + state + if index >= 0 {
"&scope=" + strings.Join(scope, "+") + return &config.Authentication.Clients[index]
"&audience=http://127.0.0.1:4444/oauth2/token"
}
func (client *Client) InitiateLoginFlow(loginUrl string) error {
// kratos: GET /self-service/login/api
req, err := http.NewRequest("GET", loginUrl, bytes.NewBuffer([]byte{}))
if err != nil {
return fmt.Errorf("failed to make request: %v", err)
}
res, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to do request: %v", err)
}
defer res.Body.Close()
// get the flow ID from response
body, err := io.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %v", err)
}
var flowData map[string]any
err = json.Unmarshal(body, &flowData)
if err != nil {
return fmt.Errorf("failed to unmarshal flow data: %v\n%v", err, string(body))
} else {
client.FlowId = flowData["id"].(string)
} }
return nil return nil
} }
func (client *Client) FetchFlowData(flowUrl string) (map[string]any, error) { func NewClientWithConfigByProvider(config *Config, issuer string) *Client {
//kratos: GET /self-service/login/flows?id={flowId} index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
return c.Issuer == issuer
})
// replace {id} in string with actual value if index >= 0 {
flowUrl = strings.ReplaceAll(flowUrl, "{id}", client.FlowId) return &config.Authentication.Clients[index]
req, err := http.NewRequest("GET", flowUrl, nil)
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
} }
res, err := client.Do(req) return nil
if err != nil {
return nil, fmt.Errorf("failed to do request: %v", err)
}
defer res.Body.Close()
// get the flow data from response
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
var flowData map[string]any
err = json.Unmarshal(body, &flowData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal flow data: %v", err)
}
return flowData, nil
} }
func (client *Client) FetchCSRFToken(flowUrl string) error { func NewClientWithConfigById(config *Config, id string) *Client {
data, err := client.FetchFlowData(flowUrl) index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
if err != nil { return c.Id == id
return fmt.Errorf("failed to fetch flow data: %v", err) })
if index >= 0 {
return &config.Authentication.Clients[index]
} }
return nil
// iterate through nodes and extract the CSRF token attribute from the flow data
ui := data["ui"].(map[string]any)
nodes := ui["nodes"].([]any)
for _, node := range nodes {
attrs := node.(map[string]any)["attributes"].(map[string]any)
name := attrs["name"].(string)
if name == "csrf_token" {
client.CsrfToken = attrs["value"].(string)
return nil
}
}
return fmt.Errorf("failed to extract CSRF token: not found")
}
func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) {
data := url.Values{
"grant_type": {"authorization_code"},
"client_id": {client.Id},
"client_secret": {client.Secret},
"redirect_uri": {strings.Join(client.RedirectUris, ",")},
}
// add optional params if valid
if code != "" {
data["code"] = []string{code}
}
if state != "" {
data["state"] = []string{state}
}
res, err := http.PostForm(remoteUrl, data)
if err != nil {
return nil, fmt.Errorf("failed to get ID token: %s", err)
}
defer res.Body.Close()
domain, _ := url.Parse("http://127.0.0.1")
client.Jar.SetCookies(domain, res.Cookies())
return io.ReadAll(res.Body)
}
func (client *Client) FetchTokenFromAuthorizationServer(remoteUrl string, jwt string, scope []string) ([]byte, error) {
// hydra endpoint: /oauth/token
data := "grant_type=" + url.QueryEscape("urn:ietf:params:oauth:grant-type:jwt-bearer") +
"&client_id=" + client.Id +
"&client_secret=" + client.Secret
// add optional params if valid
if jwt != "" {
data += "&assertion=" + jwt
}
if scope != nil || len(scope) > 0 {
data += "&scope=" + strings.Join(scope, "+")
}
fmt.Printf("encoded params: %v\n\n", data)
req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer([]byte(data)))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
if err != nil {
return nil, fmt.Errorf("failed to make request: %s", err)
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to do request: %v", err)
}
defer res.Body.Close()
// set flow ID back to empty string to indicate a completed flow
client.FlowId = ""
return io.ReadAll(res.Body)
}
func (client *Client) AddTrustedIssuer(remoteUrl string, idp *oidc.IdentityProvider, subject string, duration time.Duration, scope []string) ([]byte, error) {
// hydra endpoint: POST /admin/trust/grants/jwt-bearer/issuers
if idp == nil {
return nil, fmt.Errorf("identity provided is nil")
}
jwkstr, err := json.Marshal(idp.Key)
if err != nil {
return nil, fmt.Errorf("failed to marshal JWK: %v", err)
}
quotedScopes := make([]string, len(scope))
for i, s := range scope {
quotedScopes[i] = fmt.Sprintf("\"%s\"", s)
}
// NOTE: Can also include "jwks_uri" instead
data := []byte(fmt.Sprintf("{"+
"\"allow_any_subject\": false,"+
"\"issuer\": \"%s\","+
"\"subject\": \"%s\","+
"\"expires_at\": \"%v\","+
"\"jwk\": %v,"+
"\"scope\": [ %s ]"+
"}", idp.Issuer, subject, time.Now().Add(duration).Format(time.RFC3339), string(jwkstr), strings.Join(quotedScopes, ",")))
fmt.Printf("%v\n", string(data))
req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data))
// req.Header.Add("X-CSRF-Token", client.CsrfToken.Value)
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
}
req.Header.Add("Content-Type", "application/json")
// req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken))
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to do request: %v", err)
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func (client *Client) AuthorizeClient(authorizeUrl string) ([]byte, error) {
// encode ID and secret for authorization header basic authentication
basicAuth := util.EncodeBase64(
fmt.Sprintf("%s:%s",
url.QueryEscape(client.Id),
url.QueryEscape(client.Secret),
),
)
body := httpx.Body("grant_type=client_credentials&scope=read")
headers := httpx.Headers{
"Authorization": basicAuth,
"Content-Type": "application/x-www-form-urlencoded",
}
_, b, err := httpx.MakeHTTPRequest(authorizeUrl, http.MethodPost, body, headers)
if err != nil {
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
}
return b, nil
}
func (client *Client) RegisterOAuthClient(registerUrl string, audience []string) ([]byte, error) {
// hydra endpoint: POST /clients
audience = util.QuoteArrayStrings(audience)
data := []byte(fmt.Sprintf(`{
"client_name": "%s",
"client_secret": "%s",
"token_endpoint_auth_method": "client_secret_post",
"scope": "openid email profile",
"grant_types": ["client_credentials", "urn:ietf:params:oauth:grant-type:jwt-bearer"],
"response_types": ["token"],
}`, client.Id, client.Secret,
// strings.Join(audience, ",")
))
// "audience": [%s]
req, err := http.NewRequest("POST", registerUrl, bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("failed to make request: %v", err)
}
req.Header.Add("Content-Type", "application/json")
// req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken))
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to do request: %v", err)
}
defer res.Body.Close()
return io.ReadAll(res.Body)
}
func (client *Client) CreateIdentity(remoteUrl string, idToken string) ([]byte, error) {
// kratos endpoint: /admin/identities
data := []byte(`{
"schema_id": "preset://email",
"traits": {
"email": "docs@example.org"
}
}`)
req, err := http.NewRequest("POST", remoteUrl, bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("failed to create a new request: %v", err)
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", idToken))
// req.Header.Add("X-CSRF-Token", client.CsrfToken.Value)
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to do request: %v", err)
}
return io.ReadAll(res.Body)
}
func (client *Client) FetchIdentities(remoteUrl string) ([]byte, error) {
req, err := http.NewRequest("GET", remoteUrl, bytes.NewBuffer([]byte{}))
if err != nil {
return nil, fmt.Errorf("failed to create a new request: %v", err)
}
res, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to do request: %v", err)
}
return io.ReadAll(res.Body)
} }
func (client *Client) ClearCookies() { func (client *Client) ClearCookies() {

View file

@ -1,7 +1,6 @@
package opaal package opaal
import ( import (
"davidallendj/opaal/internal/oidc"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@ -11,20 +10,46 @@ import (
"gopkg.in/yaml.v2" "gopkg.in/yaml.v2"
) )
type FlowOptions map[string]string
type Flows map[string]FlowOptions
type Providers map[string]string
type Options struct {
DecodeIdToken bool `yaml:"decode-id-token"`
DecodeAccessToken bool `yaml:"decode-access-token"`
RunOnce bool `yaml:"run-once"`
OpenBrowser bool `yaml:"open-browser"`
FlowType string `yaml:"flow"`
CachePath string `yaml:"cache"`
LocalOnly bool `yaml:"local-only"`
}
type RequestUrls struct {
Identities string `yaml:"identities"`
TrustedIssuers string `yaml:"trusted-issuers"`
Login string `yaml:"login"`
Clients string `yaml:"clients"`
Token string `yaml:"token"`
Authorize string `yaml:"authorize"`
Register string `yaml:"register"`
}
type Authentication struct {
Clients []Client `yaml:"clients"`
Flows Flows `yaml:"flows"`
}
type Authorization struct {
RequestUrls RequestUrls `yaml:"urls"`
}
type Config struct { type Config struct {
Version string `yaml:"version"` Version string `yaml:"version"`
Server Server `yaml:"server"` Server Server `yaml:"server"`
Client Client `yaml:"client"` Providers Providers `yaml:"providers"`
IdentityProvider oidc.IdentityProvider `yaml:"oidc"` Options Options `yaml:"options"`
State string `yaml:"state"` Authentication Authentication `yaml:"authentication"`
ResponseType string `yaml:"response-type"` Authorization Authorization `yaml:"authorization"`
Scope []string `yaml:"scope"`
ActionUrls ActionUrls `yaml:"urls"`
OpenBrowser bool `yaml:"open-browser"`
DecodeIdToken bool `yaml:"decode-id-token"`
DecodeAccessToken bool `yaml:"decode-access-token"`
RunOnce bool `yaml:"run-once"`
GrantType string `yaml:"grant-type"`
} }
func NewConfig() Config { func NewConfig() Config {
@ -34,31 +59,17 @@ func NewConfig() Config {
Host: "127.0.0.1", Host: "127.0.0.1",
Port: 3333, Port: 3333,
}, },
Client: Client{ Options: Options{
Id: "", DecodeIdToken: true,
Secret: "", DecodeAccessToken: true,
RedirectUris: []string{""}, RunOnce: true,
OpenBrowser: false,
CachePath: "opaal.db",
FlowType: "authorization_code",
LocalOnly: false,
}, },
IdentityProvider: *oidc.NewIdentityProvider(), Authentication: Authentication{},
State: goutil.RandomString(20), Authorization: Authorization{},
ResponseType: "code",
Scope: []string{"openid", "profile", "email"},
ActionUrls: ActionUrls{
Identities: "",
AccessToken: "",
TrustedIssuers: "",
ServerConfig: "",
JwksUri: "",
Login: "",
LoginFlowId: "",
RegisterClient: "",
AuthorizeClient: "",
},
OpenBrowser: false,
DecodeIdToken: false,
DecodeAccessToken: false,
RunOnce: true,
GrantType: "authorization_code",
} }
} }
@ -94,3 +105,15 @@ func SaveDefaultConfig(path string) {
return return
} }
} }
func HasRequiredConfigParams(config *Config) bool {
// must have athe requirements to perform login
hasClients := len(config.Authentication.Clients) > 0
hasServer := config.Server.Host != "" && config.Server.Port != 0 && config.Server.Callback != ""
hasEndpoints := config.Authorization.RequestUrls.TrustedIssuers != "" &&
config.Authorization.RequestUrls.Login != "" &&
config.Authorization.RequestUrls.Clients != "" &&
config.Authorization.RequestUrls.Authorize != "" &&
config.Authorization.RequestUrls.Token != ""
return hasClients && hasServer && hasEndpoints
}

View file

@ -1,254 +0,0 @@
package flows
import (
opaal "davidallendj/opaal/internal"
"davidallendj/opaal/internal/oidc"
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"time"
"github.com/davidallendj/go-utils/util"
)
func AuthorizationCode(config *opaal.Config, server *opaal.Server, client *opaal.Client) error {
// initiate the login flow and get a flow ID and CSRF token
{
err := client.InitiateLoginFlow(config.ActionUrls.Login)
if err != nil {
return fmt.Errorf("failed to initiate login flow: %v", err)
}
err = client.FetchCSRFToken(config.ActionUrls.LoginFlowId)
if err != nil {
return fmt.Errorf("failed to fetch CSRF token: %v", err)
}
}
// try and fetch server configuration if provided URL
idp := oidc.NewIdentityProvider()
if config.ActionUrls.ServerConfig != "" {
fmt.Printf("Fetching server configuration: %s\n", config.ActionUrls.ServerConfig)
err := idp.FetchServerConfig(config.ActionUrls.ServerConfig)
if err != nil {
return fmt.Errorf("failed to fetch server config: %v", err)
}
} else {
// otherwise, use what's provided in config file
idp.Issuer = config.IdentityProvider.Issuer
idp.Endpoints = config.IdentityProvider.Endpoints
idp.Supported = config.IdentityProvider.Supported
}
// check if all appropriate parameters are set in config
if !opaal.HasRequiredParams(config) {
return fmt.Errorf("client ID must be set")
}
// build the authorization URL to redirect user for social sign-in
var authorizationUrl = client.BuildAuthorizationUrl(
idp.Endpoints.Authorization,
config.State,
config.ResponseType,
config.Scope,
)
// print the authorization URL for sharing
fmt.Printf("Login with identity provider:\n\n %s/login\n %s\n\n",
server.GetListenAddr(), authorizationUrl,
)
// automatically open browser to initiate login flow (only useful for testing)
if config.OpenBrowser {
util.OpenUrl(authorizationUrl)
}
// authorize oauth client and listen for callback from provider
fmt.Printf("Waiting for authorization code redirect @%s/oidc/callback...\n", server.GetListenAddr())
code, err := server.WaitForAuthorizationCode(authorizationUrl)
if errors.Is(err, http.ErrServerClosed) {
fmt.Printf("\n=========================================\nServer closed.\n=========================================\n\n")
} else if err != nil {
return fmt.Errorf("failed to start server: %s", err)
}
if client == nil {
fmt.Printf("client did not initialize\n")
}
// start up another serve in background to listen for success or failures
d := make(chan []byte)
quit := make(chan bool)
var access_token []byte
go server.Serve(d)
go func() {
select {
case <-d:
fmt.Printf("got access token")
quit <- true
case <-quit:
close(d)
close(quit)
return
default:
}
}()
// use code from response and exchange for bearer token (with ID token)
bearerToken, err := client.FetchTokenFromAuthenticationServer(
code,
idp.Endpoints.Token,
config.State,
)
if err != nil {
return fmt.Errorf("failed to fetch token from issuer: %v", err)
}
// unmarshal data to get id_token and access_token
var data map[string]any
err = json.Unmarshal([]byte(bearerToken), &data)
if err != nil || data == nil {
return fmt.Errorf("failed to unmarshal token: %v", err)
}
// extract ID token from bearer as JSON string for easy consumption
idToken := data["id_token"].(string)
idJwtSegments, err := util.DecodeJwt(idToken)
if err != nil {
fmt.Printf("failed to parse ID token: %v\n", err)
} else {
fmt.Printf("id_token: %v\n", idToken)
if config.DecodeIdToken {
if err != nil {
fmt.Printf("failed to decode JWT: %v\n", err)
} else {
for i, segment := range idJwtSegments {
// don't print last segment (signatures)
if i == len(idJwtSegments)-1 {
break
}
fmt.Printf("%s\n", string(segment))
}
}
}
fmt.Println()
}
// extract the access token to get the scopes
accessToken := data["access_token"].(string)
accessJwtSegments, err := util.DecodeJwt(accessToken)
if err != nil || len(accessJwtSegments) <= 0 {
fmt.Printf("failed to parse access token: %v\n", err)
} else {
fmt.Printf("access_token: %v\n", accessToken)
if config.DecodeIdToken {
if err != nil {
fmt.Printf("failed to decode JWT: %v\n", err)
} else {
for i, segment := range accessJwtSegments {
// don't print last segment (signatures)
if i == len(accessJwtSegments)-1 {
break
}
fmt.Printf("%s\n", string(segment))
}
}
}
fmt.Println()
}
// extract the scope from access token claims
// var scope []string
// var accessJsonPayload map[string]any
// var accessJwtPayload []byte = accessJwtSegments[1]
// if accessJsonPayload != nil {
// err := json.Unmarshal(accessJwtPayload, &accessJsonPayload)
// if err != nil {
// return fmt.Errorf("failed to unmarshal JWT: %v", err)
// }
// scope = idJsonPayload["scope"].([]string)
// }
// create a new identity with identity and session manager if url is provided
if config.ActionUrls.Identities != "" {
fmt.Printf("Attempting to create a new identity...\n")
_, err := client.CreateIdentity(config.ActionUrls.Identities, idToken)
if err != nil {
return fmt.Errorf("failed to create new identity: %v", err)
}
_, err = client.FetchIdentities(config.ActionUrls.Identities)
if err != nil {
return fmt.Errorf("failed to fetch identities: %v", err)
}
fmt.Printf("Created new identity successfully.\n\n")
}
// extract the subject from ID token claims
var subject string
var audience []string
var idJsonPayload map[string]any
var idJwtPayload []byte = idJwtSegments[1]
if idJwtPayload != nil {
err := json.Unmarshal(idJwtPayload, &idJsonPayload)
if err != nil {
return fmt.Errorf("failed to unmarshal JWT: %v", err)
}
subject = idJsonPayload["sub"].(string)
audType := reflect.ValueOf(idJsonPayload["aud"])
switch audType.Kind() {
case reflect.String:
audience = append(audience, idJsonPayload["aud"].(string))
case reflect.Array:
audience = idJsonPayload["aud"].([]string)
}
} else {
return fmt.Errorf("failed to extract subject from ID token claims")
}
// fetch JWKS and add issuer to authentication server to submit ID token
fmt.Printf("Fetching JWKS from authentication server for verification...\n")
err = idp.FetchJwk(config.ActionUrls.JwksUri)
if err != nil {
return fmt.Errorf("failed to fetch JWK: %v", err)
} else {
fmt.Printf("Successfully retrieved JWK from authentication server.\n\n")
fmt.Printf("Attempting to add issuer to authorization server...\n")
res, err := client.AddTrustedIssuer(config.ActionUrls.TrustedIssuers, idp, subject, time.Duration(1000), config.Scope)
if err != nil {
return fmt.Errorf("failed to add trusted issuer: %v", err)
}
fmt.Printf("%v\n", string(res))
}
// try and register a new client with authorization server
fmt.Printf("Registering new OAuth2 client with authorization server...\n")
res, err := client.RegisterOAuthClient("http://127.0.0.1:4445/clients", audience)
if err != nil {
return fmt.Errorf("failed to register client: %v", err)
}
fmt.Printf("%v\n", string(res))
// extract the client info from response
var clientData map[string]any
err = json.Unmarshal(res, &clientData)
if err != nil {
return fmt.Errorf("failed to unmarshal client data: %v", err)
} else {
client.Id = clientData["client_id"].(string)
client.Secret = clientData["client_secret"].(string)
}
// use ID token/user info to fetch access token from authentication server
if config.ActionUrls.AccessToken != "" {
fmt.Printf("Fetching access token from authorization server...\n")
res, err := client.FetchTokenFromAuthorizationServer(config.ActionUrls.AccessToken, idToken, config.Scope)
if err != nil {
return fmt.Errorf("failed to fetch access token: %v", err)
}
fmt.Printf("%s\n", res)
}
d <- access_token
return nil
}

View file

@ -1,29 +0,0 @@
package flows
import (
opaal "davidallendj/opaal/internal"
"fmt"
)
func ClientCredentials(config *opaal.Config, server *opaal.Server, client *opaal.Client) error {
// register a new OAuth 2 client with authorization srever
_, err := client.RegisterOAuthClient(config.ActionUrls.RegisterClient, nil)
if err != nil {
return fmt.Errorf("failed to register OAuth client: %v", err)
}
// authorize the client
_, err = client.AuthorizeClient(config.ActionUrls.AuthorizeClient)
if err != nil {
return fmt.Errorf("failed to authorize client: %v", err)
}
// request a token from the authorization server
res, err := client.FetchTokenFromAuthorizationServer(config.ActionUrls.AccessToken, "", nil)
if err != nil {
return fmt.Errorf("failed to fetch token from authorization server: %v", err)
}
fmt.Printf("token: %v\n", string(res))
return nil
}

View file

@ -1,28 +0,0 @@
package flows
import (
opaal "davidallendj/opaal/internal"
"fmt"
)
func Login(config *opaal.Config) error {
if config == nil {
return fmt.Errorf("config is not valid")
}
// initialize client that will be used throughout login flow
server := opaal.NewServerWithConfig(config)
client := opaal.NewClientWithConfig(config)
fmt.Printf("grant type: %v\n", config.GrantType)
if config.GrantType == "authorization_code" {
AuthorizationCode(config, server, client)
} else if config.GrantType == "client_credentials" {
ClientCredentials(config, server, client)
} else {
return fmt.Errorf("invalid grant type")
}
return nil
}

32
internal/identities.go Normal file
View file

@ -0,0 +1,32 @@
package opaal
import (
"fmt"
"net/http"
"github.com/davidallendj/go-utils/httpx"
)
func (client *Client) CreateIdentity(url string, idToken string) error {
// kratos endpoint: /admin/identities
body := []byte(`{
"schema_id": "preset://email",
"traits": {
"email": "docs@example.org"
}
}`)
headers := httpx.Headers{
"Content-Type": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", idToken),
}
_, _, err := httpx.MakeHttpRequest(url, http.MethodPost, body, headers)
if err != nil {
return fmt.Errorf("failed to read response body: %v", err)
}
return nil
}
func (client *Client) FetchIdentities(remoteUrl string) ([]byte, error) {
_, b, err := httpx.MakeHttpRequest(remoteUrl, http.MethodGet, []byte{}, httpx.Headers{})
return b, err
}

37
internal/login.go Normal file
View file

@ -0,0 +1,37 @@
package opaal
import (
"davidallendj/opaal/internal/db"
"davidallendj/opaal/internal/oidc"
"fmt"
)
func Login(config *Config, client *Client, provider *oidc.IdentityProvider) error {
if config == nil {
return fmt.Errorf("config is not valid")
}
// make cache if it's not where expect
_, err := db.CreateIdentityProvidersIfNotExists(config.Options.CachePath)
if err != nil {
fmt.Printf("failed to create cache: %v\n", err)
}
if config.Options.FlowType == "authorization_code" {
// create a server if doing authorization code flow
server := NewServerWithConfig(config)
err := AuthorizationCodeWithConfig(config, server, client, provider)
if err != nil {
fmt.Printf("failed to complete authorization code flow: %v\n", err)
}
} else if config.Options.FlowType == "client_credentials" {
err := ClientCredentialsWithConfig(config, client)
if err != nil {
fmt.Printf("failed to complete client credentials flow: %v", err)
}
} else {
return fmt.Errorf("invalid grant type (options: authorization_code, client_credentials)")
}
return nil
}

View file

@ -13,29 +13,29 @@ import (
) )
type IdentityProvider struct { type IdentityProvider struct {
Issuer string `json:"issuer" yaml:"issuer"` Issuer string `db:"issuer" json:"issuer" yaml:"issuer"`
Endpoints Endpoints `json:"endpoints" yaml:"endpoints"` Endpoints Endpoints `db:"endpoints" json:"endpoints" yaml:"endpoints"`
Supported Supported `json:"supported" yaml:"supported"` Supported Supported `db:"supported" json:"supported" yaml:"supported"`
Key jwk.Key Key jwk.Key
} }
type Endpoints struct { type Endpoints struct {
Authorization string `json:"authorization_endpoint" yaml:"authorization"` Authorization string `db:"authorization_endpoint" json:"authorization_endpoint" yaml:"authorization"`
Token string `json:"token_endpoint" yaml:"token"` Token string `db:"token_endpoint" json:"token_endpoint" yaml:"token"`
Revocation string `json:"revocation_endpoint" yaml:"revocation"` Revocation string `db:"revocation_endpoint" json:"revocation_endpoint" yaml:"revocation"`
Introspection string `json:"introspection_endpoint" yaml:"introspection"` Introspection string `db:"introspection_endpoint" json:"introspection_endpoint" yaml:"introspection"`
UserInfo string `json:"userinfo_endpoint" yaml:"userinfo"` UserInfo string `db:"userinfo_endpoint" json:"userinfo_endpoint" yaml:"userinfo"`
Jwks string `json:"jwks_uri" yaml:"jwks_uri"` Jwks string `db:"jwks_uri" json:"jwks_uri" yaml:"jwks_uri"`
} }
type Supported struct { type Supported struct {
ResponseTypes []string `json:"response_types_supported"` ResponseTypes []string `db:"response_types_supported" json:"response_types_supported"`
ResponseModes []string `json:"response_modes_supported"` ResponseModes []string `db:"response_modes_supported" json:"response_modes_supported"`
GrantTypes []string `json:"grant_types_supported"` GrantTypes []string `db:"grant_types_supported" json:"grant_types_supported"`
TokenEndpointAuthMethods []string `json:"token_endpoint_auth_methods_supported"` TokenEndpointAuthMethods []string `db:"token_endpoint_auth_methods_supported" json:"token_endpoint_auth_methods_supported"`
SubjectTypes []string `json:"subject_types_supported"` SubjectTypes []string `db:"subject_types_supported" json:"subject_types_supported"`
IdTokenSigningAlgValues []string `json:"id_token_signing_alg_values_supported"` IdTokenSigningAlgValues []string `db:"id_token_signing_alg_values_supported" json:"id_token_signing_alg_values_supported"`
ClaimTypes []string `json:"claim_types_supported"` ClaimTypes []string `db:"claim_types_supported" json:"claim_types_supported"`
Claims []string `json:"claims_supported"` Claims []string `db:"claims_supported" json:"claims_supported"`
} }
func NewIdentityProvider() *IdentityProvider { func NewIdentityProvider() *IdentityProvider {
@ -109,38 +109,39 @@ func (p *IdentityProvider) LoadServerConfig(path string) error {
return nil return nil
} }
func (p *IdentityProvider) FetchServerConfig(url string) error { func FetchServerConfig(issuer string) (*IdentityProvider, error) {
// make a request to a server's openid-configuration // make a request to a server's openid-configuration
req, err := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) req, err := http.NewRequest(http.MethodGet, issuer+"/.well-known/openid-configuration", bytes.NewBuffer([]byte{}))
if err != nil { if err != nil {
return fmt.Errorf("failed to create a new request: %v", err) return nil, fmt.Errorf("failed to create a new request: %v", err)
} }
client := &http.Client{} // temp client to get info and not used in flow client := &http.Client{} // temp client to get info and not used in flow
res, err := client.Do(req) res, err := client.Do(req)
if err != nil { if err != nil {
return fmt.Errorf("failed to do request: %v", err) return nil, fmt.Errorf("failed to do request: %v", err)
} }
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
return fmt.Errorf("failed to read response body: %v", err) return nil, fmt.Errorf("failed to read response body: %v", err)
} }
var p IdentityProvider
err = p.ParseServerConfig(body) err = p.ParseServerConfig(body)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse server config: %v", err) return nil, fmt.Errorf("failed to parse server config: %v", err)
} }
return nil return &p, nil
} }
func (p *IdentityProvider) FetchJwk(url string) error { func (p *IdentityProvider) FetchJwk() error {
if url == "" { if p.Endpoints.Jwks == "" {
url = p.Endpoints.Jwks return fmt.Errorf("JWKS endpoint not set")
} }
// fetch JWKS from identity provider // fetch JWKS from identity provider
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
set, err := jwk.Fetch(ctx, url) set, err := jwk.Fetch(ctx, p.Endpoints.Jwks)
if err != nil { if err != nil {
return fmt.Errorf("%v", err) return fmt.Errorf("%v", err)
} }

View file

@ -1,17 +0,0 @@
package opaal
type ActionUrls struct {
Identities string `yaml:"identities"`
TrustedIssuers string `yaml:"trusted-issuers"`
AccessToken string `yaml:"access-token"`
ServerConfig string `yaml:"server-config"`
JwksUri string `yaml:"jwks_uri"`
Login string `yaml:"login"`
LoginFlowId string `yaml:"login-flow-id"`
RegisterClient string `yaml:"register-client"`
AuthorizeClient string `yaml:"authorize-client"`
}
func HasRequiredParams(config *Config) bool {
return config.Client.Id != "" && config.Client.Secret != ""
}

View file

@ -12,13 +12,14 @@ import (
type Server struct { type Server struct {
*http.Server *http.Server
Host string `yaml:"host"` Host string `yaml:"host"`
Port int `yaml:"port"` Port int `yaml:"port"`
Callback string `yaml:"callback"`
} }
func NewServerWithConfig(config *Config) *Server { func NewServerWithConfig(conf *Config) *Server {
host := config.Server.Host host := conf.Server.Host
port := config.Server.Port port := conf.Server.Port
server := &Server{ server := &Server{
Server: &http.Server{ Server: &http.Server{
Addr: fmt.Sprintf("%s:%d", host, port), Addr: fmt.Sprintf("%s:%d", host, port),
@ -37,7 +38,12 @@ func (s *Server) GetListenAddr() string {
return fmt.Sprintf("%s:%d", s.Host, s.Port) return fmt.Sprintf("%s:%d", s.Host, s.Port)
} }
func (s *Server) WaitForAuthorizationCode(loginUrl string) (string, error) { func (s *Server) WaitForAuthorizationCode(loginUrl string, callback string) (string, error) {
// check if callback is set
if callback == "" {
callback = "/oidc/callback"
}
var code string var code string
r := chi.NewRouter() r := chi.NewRouter()
r.Use(middleware.RedirectSlashes) r.Use(middleware.RedirectSlashes)
@ -53,7 +59,7 @@ func (s *Server) WaitForAuthorizationCode(loginUrl string) (string, error) {
loginPage = []byte(strings.ReplaceAll(string(loginPage), "{{loginUrl}}", loginUrl)) loginPage = []byte(strings.ReplaceAll(string(loginPage), "{{loginUrl}}", loginUrl))
w.Write(loginPage) w.Write(loginPage)
}) })
r.HandleFunc("/oidc/callback", func(w http.ResponseWriter, r *http.Request) { r.HandleFunc(callback, func(w http.ResponseWriter, r *http.Request) {
// get the code from the OIDC provider // get the code from the OIDC provider
if r != nil { if r != nil {
code = r.URL.Query().Get("code") code = r.URL.Query().Get("code")