mirror of
https://github.com/davidallendj/opaal.git
synced 2025-12-20 03:27:02 -07:00
Refactor and added ability to use include multiple providers in config
This commit is contained in:
parent
53d1a8cc35
commit
4bca62ec2f
13 changed files with 660 additions and 712 deletions
88
config.yaml
88
config.yaml
|
|
@ -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:
|
||||||
|
facebook: "http://facebook.com"
|
||||||
|
forgejo: "http://git.towk.local:3000"
|
||||||
|
gitlab: "https://gitlab.newmexicoconsortium.org"
|
||||||
|
github: "https://github.com"
|
||||||
|
|
||||||
|
authentication:
|
||||||
|
clients:
|
||||||
|
- id: "1135541217802147"
|
||||||
|
secret: "b3a3123e8235de1dbab448369bc3d024"
|
||||||
|
issuer: "https://www.facebook.com"
|
||||||
|
scope:
|
||||||
|
- "openid"
|
||||||
|
- "name"
|
||||||
|
- "email"
|
||||||
redirect-uris:
|
redirect-uris:
|
||||||
- "http://127.0.0.1:3333/oidc/callback"
|
- "http://127.0.0.1:3333/oidc/callback"
|
||||||
oidc:
|
- id: "978b48059dd4916f53b4"
|
||||||
issuer: "http://git.towk.local:3000/"
|
secret: "eb54b533eb6afd695e3a1b3f363ab2b29acc7425"
|
||||||
urls:
|
issuer: "https://github.com"
|
||||||
|
scope:
|
||||||
|
- "openid"
|
||||||
|
- "profile"
|
||||||
|
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
|
#identities: http://127.0.0.1:4434/admin/identities
|
||||||
trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers
|
trusted-issuers: http://127.0.0.1:4445/admin/trust/grants/jwt-bearer/issuers
|
||||||
access-token: http://127.0.0.1:4444/oauth2/token
|
|
||||||
server-config: http://git.towk.local:3000/.well-known/openid-configuration
|
|
||||||
jwks_uri: http://git.towk.local:3000/login/oauth/keys
|
|
||||||
login: http://127.0.0.1:4433/self-service/login/api
|
login: http://127.0.0.1:4433/self-service/login/api
|
||||||
login-flow-id: http://127.0.0.1:4433/self-service/login/flows?id={id}
|
clients: http://127.0.0.1:4445/admin/clients
|
||||||
register-client: http://127.0.0.1:4445/clients
|
authorize: http://127.0.0.1:4444/oauth2/auth
|
||||||
authorize-client: http://127.0.0.1:4444/oauth2/authorize
|
register: http://127.0.0.1:4444/oauth2/register
|
||||||
state: ""
|
token: http://127.0.0.1:4444/oauth2/token
|
||||||
response-type: code
|
|
||||||
decode-id-token: true
|
|
||||||
decode-access-token: true
|
options:
|
||||||
run-once: true
|
decode-id-token: true
|
||||||
scope:
|
decode-access-token: true
|
||||||
- openid
|
run-once: true
|
||||||
- profile
|
open-browser: false
|
||||||
- email
|
|
||||||
|
|
|
||||||
118
internal/authenticate.go
Normal file
118
internal/authenticate.go
Normal 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
241
internal/authorize.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,11 @@
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -21,296 +13,78 @@ type Client struct {
|
||||||
http.Client
|
http.Client
|
||||||
Id string `yaml:"id"`
|
Id string `yaml:"id"`
|
||||||
Secret string `yaml:"secret"`
|
Secret string `yaml:"secret"`
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
Issuer string `yaml:"issuer"`
|
||||||
|
RegistrationAccessToken string `yaml:"registration-access-token"`
|
||||||
RedirectUris []string `yaml:"redirect-uris"`
|
RedirectUris []string `yaml:"redirect-uris"`
|
||||||
|
Scope []string `yaml:"scope"`
|
||||||
FlowId string
|
FlowId string
|
||||||
CsrfToken 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)
|
|
||||||
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 {
|
|
||||||
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 nil
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("failed to extract CSRF token: not found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) FetchTokenFromAuthenticationServer(code string, remoteUrl string, state string) ([]byte, error) {
|
func NewClientWithConfigById(config *Config, id string) *Client {
|
||||||
data := url.Values{
|
index := slices.IndexFunc(config.Authentication.Clients, func(c Client) bool {
|
||||||
"grant_type": {"authorization_code"},
|
return c.Id == id
|
||||||
"client_id": {client.Id},
|
})
|
||||||
"client_secret": {client.Secret},
|
if index >= 0 {
|
||||||
"redirect_uri": {strings.Join(client.RedirectUris, ",")},
|
return &config.Authentication.Clients[index]
|
||||||
}
|
}
|
||||||
// add optional params if valid
|
return nil
|
||||||
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() {
|
||||||
|
|
|
||||||
|
|
@ -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 Config struct {
|
type FlowOptions map[string]string
|
||||||
Version string `yaml:"version"`
|
type Flows map[string]FlowOptions
|
||||||
Server Server `yaml:"server"`
|
type Providers map[string]string
|
||||||
Client Client `yaml:"client"`
|
|
||||||
IdentityProvider oidc.IdentityProvider `yaml:"oidc"`
|
type Options struct {
|
||||||
State string `yaml:"state"`
|
|
||||||
ResponseType string `yaml:"response-type"`
|
|
||||||
Scope []string `yaml:"scope"`
|
|
||||||
ActionUrls ActionUrls `yaml:"urls"`
|
|
||||||
OpenBrowser bool `yaml:"open-browser"`
|
|
||||||
DecodeIdToken bool `yaml:"decode-id-token"`
|
DecodeIdToken bool `yaml:"decode-id-token"`
|
||||||
DecodeAccessToken bool `yaml:"decode-access-token"`
|
DecodeAccessToken bool `yaml:"decode-access-token"`
|
||||||
RunOnce bool `yaml:"run-once"`
|
RunOnce bool `yaml:"run-once"`
|
||||||
GrantType string `yaml:"grant-type"`
|
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 {
|
||||||
|
Version string `yaml:"version"`
|
||||||
|
Server Server `yaml:"server"`
|
||||||
|
Providers Providers `yaml:"providers"`
|
||||||
|
Options Options `yaml:"options"`
|
||||||
|
Authentication Authentication `yaml:"authentication"`
|
||||||
|
Authorization Authorization `yaml:"authorization"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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{""},
|
|
||||||
},
|
|
||||||
IdentityProvider: *oidc.NewIdentityProvider(),
|
|
||||||
State: goutil.RandomString(20),
|
|
||||||
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,
|
RunOnce: true,
|
||||||
GrantType: "authorization_code",
|
OpenBrowser: false,
|
||||||
|
CachePath: "opaal.db",
|
||||||
|
FlowType: "authorization_code",
|
||||||
|
LocalOnly: false,
|
||||||
|
},
|
||||||
|
Authentication: Authentication{},
|
||||||
|
Authorization: Authorization{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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
32
internal/identities.go
Normal 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
37
internal/login.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 != ""
|
|
||||||
}
|
|
||||||
|
|
@ -14,11 +14,12 @@ 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")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue