From f13dc593b04a8dfd9c91c16c7a948089407306f7 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Wed, 16 Sep 2020 22:37:19 +0000 Subject: [PATCH] WIP login flow --- mockid/mockid.go | 7 +-- mockid/mockid_test.go | 3 +- mockid/route.go | 143 +++++++++++++++++++++++++++++++++++++----- public/main.js | 31 ++++++--- 4 files changed, 152 insertions(+), 32 deletions(-) diff --git a/mockid/mockid.go b/mockid/mockid.go index 0e8674d..dd7865d 100644 --- a/mockid/mockid.go +++ b/mockid/mockid.go @@ -16,7 +16,6 @@ import ( "strconv" "time" - "git.coolaj86.com/coolaj86/go-mockid/xkeypairs" "git.rootprojects.org/root/keypairs" //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) protected64 := base64.RawURLEncoding.EncodeToString([]byte(protected)) - exp, err := xkeypairs.ParseDuration(query.Get("exp")) + exp, err := time.ParseDuration(query.Get("exp")) if nil != err { // cryptic error code // TODO propagate error @@ -113,7 +112,7 @@ func GenToken(host string, privkey keypairs.PrivateKey, query url.Values) (strin payload := fmt.Sprintf( `{"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)) @@ -133,8 +132,6 @@ func JOSESign(privkey keypairs.PrivateKey, hash []byte) []byte { case *ecdsa.PrivateKey: r, s, _ := ecdsa.Sign(rndsrc, k, hash[:]) rb := r.Bytes() - fmt.Println("debug:") - fmt.Println(r, s) for len(rb) < 32 { rb = append([]byte{0}, rb...) } diff --git a/mockid/mockid_test.go b/mockid/mockid_test.go index 80a0ebd..871c8ae 100644 --- a/mockid/mockid_test.go +++ b/mockid/mockid_test.go @@ -17,7 +17,6 @@ import ( "os" "testing" - "git.coolaj86.com/coolaj86/go-mockid/mockid/api" "git.coolaj86.com/coolaj86/go-mockid/xkeypairs" "git.rootprojects.org/root/keypairs" //keypairs "github.com/big-squid/go-keypairs" @@ -35,7 +34,7 @@ func (TestReader) Read(p []byte) (n int, err error) { var testrnd = TestReader{} func init() { - api.RandomReader = testrnd + xkeypairs.RandomReader = testrnd rndsrc = testrnd } diff --git a/mockid/route.go b/mockid/route.go index 292d91e..3b90fdf 100644 --- a/mockid/route.go +++ b/mockid/route.go @@ -43,28 +43,57 @@ type OTPResponse struct { 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 contactPrefix string // Route returns an HTTP Mux containing the full API func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { Init() + contactPrefix := jwksPrefix + "/contacts" contactKV := kvdb.KVDB{ - Prefix: jwksPrefix + "/contacts", + Prefix: contactPrefix, Ext: "eml.json", } + subjectPrefix := jwksPrefix + "/subjects" + subjectKV := kvdb.KVDB{ + Prefix: subjectPrefix, + Ext: "sub.json", + } + // TODO get from main() tokenPrefix = jwksPrefix + "/tokens" - contactPrefix = jwksPrefix + "/contacts" - for _, pre := range []string{tokenPrefix, contactPrefix} { + for _, pre := range []string{tokenPrefix, contactPrefix, subjectPrefix} { if err := os.MkdirAll(pre, 0750); nil != err { panic(err) } } 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) { if "POST" != r.Method { 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() contact := strings.Replace(strings.TrimPrefix(query.Get("contact"), "mailto:"), " ", "+", -1) if "" == contact { - fmt.Println("got here 3a") + fmt.Println("meta: empty contact query") http.Error(w, "Bad Request", http.StatusBadRequest) b, _ := json.Marshal(&HTTPResponse{ Error: "missing require query parameter 'contact'", @@ -108,8 +137,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { _, ok, err := contactKV.Load(contact) if nil != err { - fmt.Println("got here 3b") - fmt.Fprintf(os.Stderr, "bad things:", err.Error()) + fmt.Fprintf(os.Stderr, "meta: error loading contact: %s\n", err.Error()) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } @@ -128,6 +156,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { w.Write(b) }) + // TODO rename as challenge? http.HandleFunc("/api/authn/verify", func(w http.ResponseWriter, r *http.Request) { baseURL := getBaseURL(r) query := r.URL.Query() @@ -155,6 +184,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { w.Write(b) }) + // TODO rename as present? http.HandleFunc("/api/authn/consume", func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() otpSecret := query.Get("secret") @@ -193,6 +223,78 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { 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{ HTTPResponse: HTTPResponse{Success: true}, 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) return } - fmt.Fprintf(w, string(tokenB)) + fmt.Fprintf(w, string(tokenB)+"\n") }) 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 { //http.Error(w, "Not Found", http.StatusNotFound) exp := strconv.FormatInt(time.Now().Add(15*time.Minute).Unix(), 10) - jwk := string(keypairs.MarshalJWKPublicKey(pubkey)) - jwk = strings.Replace(jwk, `{"`, `{ "`, 1) - jwk = strings.Replace(jwk, `",`, `" ,`, -1) - jwk = jwk[0 : len(jwk)-1] - jwk = jwk + fmt.Sprintf(`, "ext": true , "key_ops": ["verify"], "exp": %s }`, exp) + jwkReal := string(keypairs.MarshalJWKPublicKey(signingPub)) + jwkReal = strings.Replace(jwkReal, `{"`, `{ "`, 1) + jwkReal = strings.Replace(jwkReal, `",`, `" ,`, -1) + jwkReal = jwkReal[0 : len(jwkReal)-1] + 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 } - jwkstr := fmt.Sprintf(`{ "keys": [ %s ] }`, jwk) - fmt.Println(jwkstr) + jwkstr := fmt.Sprintf(`{ "keys": [ %s, %s ] }`, jwkFake, jwkReal) + fmt.Println("jwkstr:", jwkstr) fmt.Fprintf(w, jwkstr) return } @@ -565,7 +673,7 @@ func Route(jwksPrefix string, privkey keypairs.PrivateKey) http.Handler { ` "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), ) - fmt.Println(jwkstr) + fmt.Println("jwkstr:", jwkstr) fmt.Fprintf(w, jwkstr) }) @@ -582,6 +690,7 @@ type OTP struct { SecretUA string `json:"secret_agent"` SecretIP string `json:"secret_addr"` SecretUsed time.Time `json:"secret_used"` + AccessToken string `json:"access_token,omitempty"` } 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 } diff --git a/public/main.js b/public/main.js index 59f2caf..c2afdde 100644 --- a/public/main.js +++ b/public/main.js @@ -140,14 +140,7 @@ $('.authn-email form').addEventListener('submit', function (ev) { }); }); -$('.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'); +function verifyNewDevice() { return PocketId.auth .verify({ scheme: 'mailto:', email: state.email }) .catch(function (err) { @@ -170,9 +163,31 @@ $('.authn-new-user form').addEventListener('submit', function (ev) { receipt: resp.body.receipt }) .then(function (resp) { + // this should have a token we can inspect + // and return to the calling application. + console.log(resp); 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);