WIP login flow
This commit is contained in:
parent
557f9085f6
commit
f13dc593b0
|
@ -16,7 +16,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
|
||||||
"git.rootprojects.org/root/keypairs"
|
"git.rootprojects.org/root/keypairs"
|
||||||
//jwt "github.com/dgrijalva/jwt-go"
|
//jwt "github.com/dgrijalva/jwt-go"
|
||||||
)
|
)
|
||||||
|
@ -104,7 +103,7 @@ func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (strin
|
||||||
protected := fmt.Sprintf(`{"typ":"JWT","alg":%q,"kid":"%s"}`, alg, thumbprint)
|
protected := fmt.Sprintf(`{"typ":"JWT","alg":%q,"kid":"%s"}`, alg, thumbprint)
|
||||||
protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected))
|
protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected))
|
||||||
|
|
||||||
exp, err := xkeypairs.ParseDuration(query.Get("exp"))
|
exp, err := time.ParseDuration(query.Get("exp"))
|
||||||
if nil != err {
|
if nil != err {
|
||||||
// cryptic error code
|
// cryptic error code
|
||||||
// TODO propagate error
|
// TODO propagate error
|
||||||
|
@ -113,7 +112,7 @@ func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (strin
|
||||||
|
|
||||||
payload := fmt.Sprintf(
|
payload := fmt.Sprintf(
|
||||||
`{"iss":"%s/","sub":"dummy","exp":%s}`,
|
`{"iss":"%s/","sub":"dummy","exp":%s}`,
|
||||||
host, strconv.FormatInt(time.Now().Add(time.Duration(exp)*time.Second).Unix(), 10),
|
host, strconv.FormatInt(time.Now().Add(exp*time.Second).Unix(), 10),
|
||||||
)
|
)
|
||||||
payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
payload64 := base64.RawURLEncoding.EncodeToString([]byte(payload))
|
||||||
|
|
||||||
|
@ -133,8 +132,6 @@ func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte {
|
||||||
case *ecdsa.PrivateKey:
|
case *ecdsa.PrivateKey:
|
||||||
r, s, _ := ecdsa.Sign(rndsrc, k, hash[:])
|
r, s, _ := ecdsa.Sign(rndsrc, k, hash[:])
|
||||||
rb := r.Bytes()
|
rb := r.Bytes()
|
||||||
fmt.Println("debug:")
|
|
||||||
fmt.Println(r, s)
|
|
||||||
for len(rb) < 32 {
|
for len(rb) < 32 {
|
||||||
rb = append([]byte{0}, rb...)
|
rb = append([]byte{0}, rb...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.coolaj86.com/coolaj86/go-mockid/mockid/api"
|
|
||||||
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
"git.coolaj86.com/coolaj86/go-mockid/xkeypairs"
|
||||||
"git.rootprojects.org/root/keypairs"
|
"git.rootprojects.org/root/keypairs"
|
||||||
//keypairs "github.com/big-squid/go-keypairs"
|
//keypairs "github.com/big-squid/go-keypairs"
|
||||||
|
@ -35,7 +34,7 @@ func (TestReader) Read(p []byte) (n int, err error) {
|
||||||
var testrnd = TestReader{}
|
var testrnd = TestReader{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
api.RandomReader = testrnd
|
xkeypairs.RandomReader = testrnd
|
||||||
rndsrc = testrnd
|
rndsrc = testrnd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
143
mockid/route.go
143
mockid/route.go
|
@ -43,28 +43,57 @@ type OTPResponse struct {
|
||||||
HTTPResponse
|
HTTPResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Contact struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Subjects []string `json:"subjects"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Subject struct {
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Emails map[string]time.Time `json:"emails"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var privKeyJWKPath string
|
||||||
var tokenPrefix string
|
var tokenPrefix string
|
||||||
var contactPrefix string
|
|
||||||
|
|
||||||
// Route returns an HTTP Mux containing the full API
|
// Route returns an HTTP Mux containing the full API
|
||||||
func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
Init()
|
Init()
|
||||||
|
|
||||||
|
contactPrefix := jwksPrefix + "/contacts"
|
||||||
contactKV := kvdb.KVDB{
|
contactKV := kvdb.KVDB{
|
||||||
Prefix: jwksPrefix + "/contacts",
|
Prefix: contactPrefix,
|
||||||
Ext: "eml.json",
|
Ext: "eml.json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subjectPrefix := jwksPrefix + "/subjects"
|
||||||
|
subjectKV := kvdb.KVDB{
|
||||||
|
Prefix: subjectPrefix,
|
||||||
|
Ext: "sub.json",
|
||||||
|
}
|
||||||
|
|
||||||
// TODO get from main()
|
// TODO get from main()
|
||||||
tokenPrefix = jwksPrefix + "/tokens"
|
tokenPrefix = jwksPrefix + "/tokens"
|
||||||
contactPrefix = jwksPrefix + "/contacts"
|
for _, pre := range []string{tokenPrefix, contactPrefix, subjectPrefix} {
|
||||||
for _, pre := range []string{tokenPrefix, contactPrefix} {
|
|
||||||
if err := os.MkdirAll(pre, 0750); nil != err {
|
if err := os.MkdirAll(pre, 0750); nil != err {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pubkey := keypairs.NewPublicKey(privkey.Public())
|
pubkey := keypairs.NewPublicKey(privkey.Public())
|
||||||
|
|
||||||
|
privKeyJWKPath = jwksPrefix + "/private/priv.jwk.json"
|
||||||
|
signingKey, err := xkeypairs.ParsePrivateKeyFile(privKeyJWKPath)
|
||||||
|
if nil != err {
|
||||||
|
signingKey = xkeypairs.GenPrivKey(&xkeypairs.KeyOptions{})
|
||||||
|
_ = os.MkdirAll(jwksPrefix+"/private", 0750)
|
||||||
|
b := xkeypairs.MarshalJWKPrivateKey(signingKey)
|
||||||
|
if err := ioutil.WriteFile(privKeyJWKPath, b, 0600); nil != err {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
signingPub := keypairs.NewPublicKey(signingKey.Public())
|
||||||
|
//signingKid := keypairs.ThumbprintPublicKey(signingPub)
|
||||||
|
|
||||||
http.HandleFunc("/api/new-hashcash", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/api/new-hashcash", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if "POST" != r.Method {
|
if "POST" != r.Method {
|
||||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||||
|
@ -97,7 +126,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
contact := strings.Replace(strings.TrimPrefix(query.Get("contact"), "mailto:"), " ", "+", -1)
|
contact := strings.Replace(strings.TrimPrefix(query.Get("contact"), "mailto:"), " ", "+", -1)
|
||||||
if "" == contact {
|
if "" == contact {
|
||||||
fmt.Println("got here 3a")
|
fmt.Println("meta: empty contact query")
|
||||||
http.Error(w, "Bad Request", http.StatusBadRequest)
|
http.Error(w, "Bad Request", http.StatusBadRequest)
|
||||||
b, _ := json.Marshal(&HTTPResponse{
|
b, _ := json.Marshal(&HTTPResponse{
|
||||||
Error: "missing require query parameter 'contact'",
|
Error: "missing require query parameter 'contact'",
|
||||||
|
@ -108,8 +137,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
|
|
||||||
_, ok, err := contactKV.Load(contact)
|
_, ok, err := contactKV.Load(contact)
|
||||||
if nil != err {
|
if nil != err {
|
||||||
fmt.Println("got here 3b")
|
fmt.Fprintf(os.Stderr, "meta: error loading contact: %s\n", err.Error())
|
||||||
fmt.Fprintf(os.Stderr, "bad things:", err.Error())
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -128,6 +156,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
w.Write(b)
|
w.Write(b)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO rename as challenge?
|
||||||
http.HandleFunc("/api/authn/verify", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/api/authn/verify", func(w http.ResponseWriter, r *http.Request) {
|
||||||
baseURL := getBaseURL(r)
|
baseURL := getBaseURL(r)
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
|
@ -155,6 +184,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
w.Write(b)
|
w.Write(b)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO rename as present?
|
||||||
http.HandleFunc("/api/authn/consume", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/api/authn/consume", func(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query()
|
query := r.URL.Query()
|
||||||
otpSecret := query.Get("secret")
|
otpSecret := query.Get("secret")
|
||||||
|
@ -193,6 +223,78 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var contact *Contact
|
||||||
|
val, ok, err := contactKV.Load(otp.Email, &Contact{})
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
fmt.Println("error loading contact:", err)
|
||||||
|
fmt.Fprintf(w, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
contact = val.(*Contact)
|
||||||
|
} else {
|
||||||
|
// TODO c := Contacts.New(); c.Save();
|
||||||
|
uuid, _ := uuid.NewRandom()
|
||||||
|
uuidBin, _ := uuid.MarshalBinary()
|
||||||
|
sub := base64.RawURLEncoding.EncodeToString(uuidBin)
|
||||||
|
contact = &Contact{
|
||||||
|
Email: otp.Email,
|
||||||
|
Subjects: []string{sub},
|
||||||
|
}
|
||||||
|
if err := contactKV.Store(otp.Email, contact); nil != err {
|
||||||
|
fmt.Println("ERROR contactKV Store", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
subject := &Subject{
|
||||||
|
Subject: sub,
|
||||||
|
Emails: map[string]time.Time{
|
||||||
|
otp.Email: time.Now(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := subjectKV.Store(sub, subject); nil != err {
|
||||||
|
fmt.Println("ERROR subjectKV Store", err)
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sub string
|
||||||
|
if len(contact.Subjects) < 1 || len(contact.Subjects) > 1 {
|
||||||
|
fmt.Printf("%#v\n", contact)
|
||||||
|
if len(contact.Subjects) > 1 {
|
||||||
|
fmt.Fprintf(os.Stderr, "TODO: give exchange token for multiple accounts\n")
|
||||||
|
}
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sub = contact.Subjects[0]
|
||||||
|
|
||||||
|
uuid, _ := uuid.NewRandom()
|
||||||
|
nonce, _ := uuid.MarshalBinary()
|
||||||
|
baseURL := getBaseURL(r)
|
||||||
|
tok, err := xkeypairs.SignClaims(
|
||||||
|
signingKey,
|
||||||
|
xkeypairs.Object{},
|
||||||
|
xkeypairs.Object{
|
||||||
|
"sub": sub,
|
||||||
|
"iss": baseURL,
|
||||||
|
"jti": base64.RawURLEncoding.EncodeToString(nonce),
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
|
"exp": "1h",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if nil != err {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
fmt.Fprintf(w, "%s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
otp.AccessToken = xkeypairs.JWSToJWT(tok)
|
||||||
b, _ := json.Marshal(&OTPResponse{
|
b, _ := json.Marshal(&OTPResponse{
|
||||||
HTTPResponse: HTTPResponse{Success: true},
|
HTTPResponse: HTTPResponse{Success: true},
|
||||||
OTP: *otp,
|
OTP: *otp,
|
||||||
|
@ -488,7 +590,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
http.Error(w, "Bad Format: malformed token, or malformed jwk at issuer url", http.StatusInternalServerError)
|
http.Error(w, "Bad Format: malformed token, or malformed jwk at issuer url", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, string(tokenB))
|
fmt.Fprintf(w, string(tokenB)+"\n")
|
||||||
})
|
})
|
||||||
|
|
||||||
http.HandleFunc("/authorization_header", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/authorization_header", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -540,14 +642,20 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
if nil != err {
|
if nil != err {
|
||||||
//http.Error(w, "Not Found", http.StatusNotFound)
|
//http.Error(w, "Not Found", http.StatusNotFound)
|
||||||
exp := strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10)
|
exp := strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10)
|
||||||
jwk := string(keypairs.MarshalJWKPublicKey(pubkey))
|
jwkReal := string(keypairs.MarshalJWKPublicKey(signingPub))
|
||||||
jwk = strings.Replace(jwk, `{"`, `{ "`, 1)
|
jwkReal = strings.Replace(jwkReal, `{"`, `{ "`, 1)
|
||||||
jwk = strings.Replace(jwk, `",`, `" ,`, -1)
|
jwkReal = strings.Replace(jwkReal, `",`, `" ,`, -1)
|
||||||
jwk = jwk[0 : len(jwk)-1]
|
jwkReal = jwkReal[0 : len(jwkReal)-1]
|
||||||
jwk = jwk + fmt.Sprintf(`, "ext": true , "key_ops": ["verify"], "exp": %s }`, exp)
|
jwkReal = jwkReal + fmt.Sprintf(`, "ext": true , "key_ops": ["verify"], "exp": %s, "REAL": true }`, exp)
|
||||||
|
|
||||||
|
jwkFake := string(keypairs.MarshalJWKPublicKey(pubkey))
|
||||||
|
jwkFake = strings.Replace(jwkFake, `{"`, `{ "`, 1)
|
||||||
|
jwkFake = strings.Replace(jwkFake, `",`, `" ,`, -1)
|
||||||
|
jwkFake = jwkFake[0 : len(jwkFake)-1]
|
||||||
|
jwkFake = jwkFake + fmt.Sprintf(`, "ext": true , "key_ops": ["verify"], "exp": %s, "FAKE": true }`, exp)
|
||||||
// { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s }
|
// { "kty": "EC" , "crv": %q , "x": %q , "y": %q , "kid": %q , "ext": true , "key_ops": ["verify"] , "exp": %s }
|
||||||
jwkstr := fmt.Sprintf(`{ "keys": [ %s ] }`, jwk)
|
jwkstr := fmt.Sprintf(`{ "keys": [ %s, %s ] }`, jwkFake, jwkReal)
|
||||||
fmt.Println(jwkstr)
|
fmt.Println("jwkstr:", jwkstr)
|
||||||
fmt.Fprintf(w, jwkstr)
|
fmt.Fprintf(w, jwkstr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -565,7 +673,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler {
|
||||||
` "ext": true, "key_ops": ["verify"], "exp": %s } ] }`,
|
` "ext": true, "key_ops": ["verify"], "exp": %s } ] }`,
|
||||||
tok.Crv, tok.X, tok.Y, tok.KeyID, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10),
|
tok.Crv, tok.X, tok.Y, tok.KeyID, strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10),
|
||||||
)
|
)
|
||||||
fmt.Println(jwkstr)
|
fmt.Println("jwkstr:", jwkstr)
|
||||||
fmt.Fprintf(w, jwkstr)
|
fmt.Fprintf(w, jwkstr)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -582,6 +690,7 @@ type OTP struct {
|
||||||
SecretUA string `json:"secret_agent"`
|
SecretUA string `json:"secret_agent"`
|
||||||
SecretIP string `json:"secret_addr"`
|
SecretIP string `json:"secret_addr"`
|
||||||
SecretUsed time.Time `json:"secret_used"`
|
SecretUsed time.Time `json:"secret_used"`
|
||||||
|
AccessToken string `json:"access_token,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func hashOTPSecret(secret []byte) (hash string, receipt string) {
|
func hashOTPSecret(secret []byte) (hash string, receipt string) {
|
||||||
|
@ -691,7 +800,7 @@ func checkOTP(hash, email, agent, addr string, consume otpConsumer) (*OTP, error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("SNTHSNTHSNTHSNTHSNTHSNTHSNTHSNTHNSTHSNTH GOOD!!")
|
fmt.Println("THE TOKEN IS GOOD. GOOD!!")
|
||||||
|
|
||||||
return &otp, nil
|
return &otp, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -140,14 +140,7 @@ $('.authn-email form').addEventListener('submit', function (ev) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.authn-new-user form').addEventListener('submit', function (ev) {
|
function verifyNewDevice() {
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
|
|
||||||
// We don't need to worry about checking if the key exists
|
|
||||||
// even if it does, the account has been deactivated
|
|
||||||
|
|
||||||
setFlow('.authn-container', '.authn-loading');
|
|
||||||
return PocketId.auth
|
return PocketId.auth
|
||||||
.verify({ scheme: 'mailto:', email: state.email })
|
.verify({ scheme: 'mailto:', email: state.email })
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
|
@ -170,9 +163,31 @@ $('.authn-new-user form').addEventListener('submit', function (ev) {
|
||||||
receipt: resp.body.receipt
|
receipt: resp.body.receipt
|
||||||
})
|
})
|
||||||
.then(function (resp) {
|
.then(function (resp) {
|
||||||
|
// this should have a token we can inspect
|
||||||
|
// and return to the calling application.
|
||||||
|
console.log(resp);
|
||||||
window.alert('all set!');
|
window.alert('all set!');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('.authn-existing form').addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
setFlow('.authn-container', '.authn-loading');
|
||||||
|
verifyNewDevice();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.authn-new-user form').addEventListener('submit', function (ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
// We don't need to worry about checking if the key exists
|
||||||
|
// even if it does, the account has been deactivated
|
||||||
|
|
||||||
|
setFlow('.authn-container', '.authn-loading');
|
||||||
|
verifyNewDevice();
|
||||||
});
|
});
|
||||||
|
|
||||||
var route = window.location.hash.split('/').slice(1);
|
var route = window.location.hash.split('/').slice(1);
|
||||||
|
|
Loading…
Reference in New Issue