Skip to content

Commit 4aa2b88

Browse files
committed
main,userdb,qlib: Add OpenID-Connect authentication for registration
1 parent f0f86c6 commit 4aa2b88

10 files changed

+398
-26
lines changed

Protocol.md

+20-8
Original file line numberDiff line numberDiff line change
@@ -97,28 +97,40 @@ After the client completes a Login or Register sequence, either side may contact
9797
0. __TmtpRev__ gives the latest recognized protocol version; it must be the first message.
9898
```
9999
{ "op": 0,
100-
"id": "1"} // protocol version string
100+
"id": "1"} // protocol version string
101101
```
102102
Response:
103103
```
104-
{ "op": "tmtprev",
105-
"id": "1"} // protocol version string
104+
{ "op": "tmtprev",
105+
"id": "1", // protocol version string
106+
"name": string, // site-specific name
107+
<"auth": 1 | 2, // 1 registration, 2 registration & login
108+
"authby": [{"label": string, // OpenID Connect provider
109+
"login": [string, <string>], // authentication URL, URL-encoded params
110+
"token": [string, <string>]}, // token URL, URL-encoded params
111+
... ]>}
106112
```
107113

108114
0. __Register__ creates a user account with a single node.
109115
_todo: accept credentials for third party authentication services_
110116
```
111117
{ "op": 1,
112-
"newnode": string, // user label for a client device
113-
<"newalias": string>} // user alias, must be 8+ printable characters
118+
"newnode": string, // user label for a client device
119+
<"newalias": string>, // user alias, must be 8+ printable characters
120+
<"oidc": // OpenID Connect token result
121+
{ "token_type": "Bearer",
122+
"expires_in": uint,
123+
"id_token": string,
124+
"access_token": string,
125+
"refresh_token": string}>}
114126
```
115127
Response: same as _Login_
116128
To sender's node:
117129
```
118130
{ "op": "registered",
119-
"uid": string, // permanent id for new user
120-
"nodeid": string, // password for first node
121-
<"error": string>} // reason alias was not allowed
131+
"uid": string, // permanent id for new user
132+
"nodeid": string, // password for first node
133+
<"error": string>} // reason alias was not allowed
122134
```
123135

124136
0. __Login__ connects a client to a user node.

README.md

+19-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ b) `kill -s INT <background_pid>` # send SIGINT signal, triggering graceful shut
6464

6565
### Configuration
6666

67-
The file mnm.config contains a JSON object with these fields.
67+
The file "mnm.config" contains a JSON object with these fields.
6868

6969
The `ntp` (network time protocol) object defines:
7070
`hosts` - an array of NTP servers
@@ -74,6 +74,24 @@ The `listen` object defines:
7474
`net` & `laddr` - arguments to `net.ListenConfig.Listen(nil, net, laddr)`
7575
`certPath` & `keyPath` - arguments to `tls.LoadX509KeyPair(certPath, keyPath)`
7676

77+
The `name` parameter defines the server's `tmtprev` response `.name` field.
78+
79+
The `auth` parameter defines where third party authentication is required:
80+
`0` - not supported
81+
`1` - required for registration
82+
`2` - required for registration and login (not yet implemented)
83+
84+
The `authby` array defines a set of objects describing OpenID Connect providers:
85+
`label` - the name of the OIDC provider/application
86+
`login` - an array giving the base URL, followed by name=value request parameters, for OIDC `/authorize`
87+
`token` - an array giving the base URL, followed by name=value request parameters, for OIDC `/token`
88+
`std` - an array of name=value request parameters to append to both `login` & `token` requests
89+
`keys` - the URL for the public key needed to validate tokens provided by OIDC authentication
90+
`iss` & `aud` - expected values for claims in the `.id_token` field of OIDC tokens
91+
92+
If the first `authby` object is empty, OpenID Connect authentication is optional.
93+
This is useful for testing.
94+
7795

7896
### Build & package
7997

main.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func mainResult() int {
5555
err = sConfig.load()
5656
if err != nil {
5757
if !os.IsNotExist(err) {
58-
fmt.Fprintf(os.Stderr, "config load: %s\n", err.Error())
58+
fmt.Fprintf(os.Stderr, "config load: %v\n", err)
5959
} else {
6060
fmt.Fprintf(os.Stderr, "config load: %s missing; see mnm.conf for example\n", kConfigFile)
6161
}
@@ -105,6 +105,9 @@ type tConfig struct {
105105
Laddr string
106106
CertPath, KeyPath string
107107
}
108+
Name string
109+
Auth byte
110+
AuthBy []pQ.TAuthBy
108111
}
109112

110113
func (o *tConfig) load() error {
@@ -113,6 +116,9 @@ func (o *tConfig) load() error {
113116
err = json.Unmarshal(aBuf, o)
114117
if err != nil { return err }
115118

119+
err = pQ.SetTmtpRev(o.Name, o.Auth, o.AuthBy) // modifies .AuthBy
120+
if err != nil { return err }
121+
116122
for _, aHost := range o.Ntp.Hosts {
117123
for a := uint8(0); a < o.Ntp.Retries; a++ {
118124
o.Ntp.time, err = pNtp.Time(aHost)

mnm.conf

+19-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,23 @@
99
"laddr": ":443",
1010
"certPath": "./server.crt",
1111
"keyPath": "./server.key"
12-
}
12+
},
13+
"name": "your-site-name",
14+
"auth": 0,
15+
"authby": null,
16+
"#authby": [{
17+
"#": "authentication optional (for testing)"
18+
},{
19+
"label": "Example OpenID Connect provider",
20+
"login": ["https://example.com/authorize",
21+
"response_type=code",
22+
"scope=openid"],
23+
"token": ["https://example.com/token",
24+
"grant_type=authorization_code"],
25+
"std": ["client_id=your-id",
26+
"redirect_uri=http://localhost:8123/u"],
27+
"keys": "https://example.com/keys",
28+
"iss": "https://example.com/",
29+
"aud": "your-id"
30+
}]
1331
}

qlib/openid.go

+234
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright 2021 Liam Breck
2+
// Published at https://github.com/networkimprov/mnm
3+
//
4+
// This Source Code Form is subject to the terms of the Mozilla Public
5+
// License, v. 2.0. If a copy of the MPL was not distributed with this
6+
// file, You can obtain one at http://mozilla.org/MPL/2.0/
7+
8+
package qlib
9+
10+
import (
11+
"encoding/base64"
12+
"math/big"
13+
"encoding/binary"
14+
"bytes"
15+
"crypto"
16+
"fmt"
17+
"net/http"
18+
"encoding/json"
19+
"os"
20+
"crypto/rand"
21+
"crypto/rsa"
22+
"crypto/sha256"
23+
"strconv"
24+
"time"
25+
)
26+
27+
var sOpenidCfg []tOpenidCfg
28+
29+
type tOpenidCfg struct {
30+
url, iss string
31+
aud string
32+
keys []tOpenidKey
33+
}
34+
35+
type tOpenidKey struct {
36+
Kty, Alg, Use string
37+
Kid string
38+
E tBase64Int
39+
N *tBase64BigInt
40+
}
41+
42+
type tBase64Int int
43+
type tBase64BigInt big.Int
44+
45+
func (o *tBase64Int) UnmarshalJSON(iStr []byte) error {
46+
iStr, err := _decodeBase64Url(iStr[1:len(iStr)-1])
47+
if err != nil { return err }
48+
if len(iStr) == 3 {
49+
iStr = append(iStr, 0)
50+
}
51+
*o = tBase64Int(binary.LittleEndian.Uint32(iStr))
52+
return nil
53+
}
54+
55+
func (o *tBase64BigInt) UnmarshalJSON(iStr []byte) error {
56+
iStr, err := _decodeBase64Url(iStr[1:len(iStr)-1])
57+
if err != nil { return err }
58+
(*big.Int)(o).SetBytes(iStr)
59+
return nil
60+
}
61+
62+
func _decodeBase64Url(iStr []byte) ([]byte, error) {
63+
aLen, err := base64.RawURLEncoding.Decode(iStr, iStr)
64+
return iStr[:aLen], err
65+
}
66+
67+
type tOpenidToken struct {
68+
Scope, Token_type string
69+
Expires_in uint
70+
Access_token, Id_token string
71+
Refresh_token string `json:",omitempty"`
72+
}
73+
74+
type tOpenidHeader struct {
75+
Kid string
76+
Alg string
77+
}
78+
79+
type tOpenidClaims struct {
80+
Ver int
81+
Sub, Aud, Idp string
82+
Iss string
83+
Iat, Exp, Auth_time tUnixTime
84+
Jti string
85+
Amr []string
86+
At_hash string
87+
}
88+
89+
type tUnixTime struct { time.Time }
90+
91+
func (o *tUnixTime) UnmarshalJSON(iStr []byte) error {
92+
aN, err := strconv.ParseUint(string(iStr), 10, 64)
93+
if err != nil { return err }
94+
*o = tUnixTime{time.Unix(int64(aN), 0).UTC()}
95+
return nil
96+
}
97+
98+
func clearConfigOpenid() { sOpenidCfg = nil }
99+
100+
func addConfigOpenid(iUrl string, iIss string, iAud string) {
101+
sOpenidCfg = append(sOpenidCfg, tOpenidCfg{url: iUrl, iss: iIss, aud: iAud})
102+
}
103+
104+
func initOpenid() {
105+
for a := range sOpenidCfg {
106+
aResp, err := http.Get(sOpenidCfg[a].url)
107+
if err != nil {
108+
fmt.Fprintf(os.Stderr, "OpenID config: could not obtain %s: %v\n restart to retry",
109+
sOpenidCfg[a].url, err)
110+
continue
111+
}
112+
var aKeys struct { Keys []tOpenidKey }
113+
err = json.NewDecoder(aResp.Body).Decode(&aKeys)
114+
aResp.Body.Close()
115+
if err != nil {
116+
fmt.Fprintf(os.Stderr, "OpenID config: could not parse response from %s: %v\n restart to retry",
117+
sOpenidCfg[a].url, err)
118+
continue
119+
}
120+
sOpenidCfg[a].keys = aKeys.Keys
121+
}
122+
}
123+
124+
func validateTokenOpenid(iTok *tOpenidToken) (tMsg, error) {
125+
var err error
126+
aSet := bytes.Split([]byte(iTok.Id_token), []byte{'.'})
127+
if len(aSet) != 3 {
128+
return nil, tError("OpenID id_token string invalid")
129+
}
130+
131+
aHash := sha256.New()
132+
aHash.Write(aSet[0])
133+
aHash.Write([]byte{'.'})
134+
aHash.Write(aSet[1])
135+
136+
for a := range aSet {
137+
aSet[a], err = _decodeBase64Url(aSet[a])
138+
if err != nil {
139+
return nil, tError("OpenID id_token base64 invalid")
140+
}
141+
}
142+
143+
var aHead tOpenidHeader
144+
err = json.Unmarshal(aSet[0], &aHead)
145+
if err != nil {
146+
return nil, tError("OpenID id_token JSON invalid")
147+
}
148+
var aClaims tOpenidClaims
149+
err = json.Unmarshal(aSet[1], &aClaims)
150+
if err != nil {
151+
return nil, tError("OpenID id_token JSON invalid")
152+
}
153+
154+
if aClaims.Exp.Before(time.Now()) {
155+
return nil, tError("OpenID id_token expired")
156+
}
157+
158+
var aCfg *tOpenidCfg
159+
for a := range sOpenidCfg {
160+
if sOpenidCfg[a].iss == aClaims.Iss && sOpenidCfg[a].aud == aClaims.Aud {
161+
aCfg = &sOpenidCfg[a]
162+
break
163+
}
164+
}
165+
if aCfg == nil {
166+
return nil, tError("OpenID id_token claims invalid")
167+
}
168+
169+
var aPk *rsa.PublicKey
170+
for a := range aCfg.keys {
171+
if aCfg.keys[a].Kid == aHead.Kid && aCfg.keys[a].Alg == aHead.Alg {
172+
aPk = &rsa.PublicKey{N: (*big.Int)(aCfg.keys[a].N), E: int(aCfg.keys[a].E)}
173+
break
174+
}
175+
}
176+
if aPk == nil {
177+
return nil, tError("OpenID key not found; restart to reload keys")
178+
}
179+
180+
err = rsa.VerifyPKCS1v15(aPk, crypto.SHA256, aHash.Sum(nil), aSet[2])
181+
if err != nil {
182+
return nil, tError("OpenID id_token signature invalid")
183+
}
184+
aMsg := tMsg{"Subject": aClaims.Sub, "Issuer": aClaims.Iss, "Audience": aClaims.Aud,
185+
"IssuedAt": aClaims.Iat.Format(time.RFC3339)}
186+
return aMsg, nil
187+
}
188+
189+
func enableTestOpenid() *tOpenidToken {
190+
aPk, err := rsa.GenerateKey(rand.Reader, 1024)
191+
if err != nil { panic(err) }
192+
193+
aKeys := struct { Keys []tMsg `json:"keys"` }{ []tMsg{{"alg":"RS256", "kid":"kid"}} }
194+
aBuf := make([]byte, 4)
195+
binary.LittleEndian.PutUint32(aBuf, uint32(aPk.E))
196+
if aBuf[3] == 0 { aBuf = aBuf[:3] }
197+
aKeys.Keys[0]["e"] = base64.RawURLEncoding.EncodeToString(aBuf)
198+
aKeys.Keys[0]["n"] = base64.RawURLEncoding.EncodeToString(aPk.N.Bytes())
199+
200+
go func() {
201+
http.HandleFunc("/keys", func(cResp http.ResponseWriter, cReq *http.Request) {
202+
cResp.Header().Set("Content-Type", "application/json")
203+
err := json.NewEncoder(cResp).Encode(&aKeys)
204+
if err != nil {
205+
fmt.Fprintf(os.Stderr, "OpenID keys test: %v\n", err)
206+
}
207+
})
208+
err := http.ListenAndServe(":8080", nil) //todo enable command-line option for port
209+
if err != http.ErrServerClosed {
210+
fmt.Fprintf(os.Stderr, "OpenID keys test: %v\n", err)
211+
}
212+
}()
213+
214+
aIdH := tMsg{"kid":"kid", "alg":"RS256"}
215+
aIdC := tMsg{"sub":"subject", "iss":"issuer", "aud":"audience",
216+
"exp": time.Now().Add(time.Minute).Unix()}
217+
aTok := tOpenidToken{Token_type:"Bearer", Expires_in:3600, Access_token:"access"}
218+
219+
aBuf, err = json.Marshal(&aIdH)
220+
if err != nil { panic(err) }
221+
aTok.Id_token = base64.RawURLEncoding.EncodeToString(aBuf)
222+
223+
aBuf, err = json.Marshal(&aIdC)
224+
if err != nil { panic(err) }
225+
aTok.Id_token += "." + base64.RawURLEncoding.EncodeToString(aBuf)
226+
227+
aHash := sha256.New()
228+
aHash.Write([]byte(aTok.Id_token))
229+
aBuf, err = rsa.SignPKCS1v15(nil, aPk, crypto.SHA256, aHash.Sum(nil))
230+
if err != nil { panic(err) }
231+
aTok.Id_token += "." + base64.RawURLEncoding.EncodeToString(aBuf)
232+
233+
return &aTok
234+
}

0 commit comments

Comments
 (0)