Skip to content
Permalink
Browse files

Add auth.

  • Loading branch information...
Jim McBeath
Jim McBeath committed Feb 15, 2018
1 parent 0a49c92 commit 44a6029b2bb42b15ccec09ff38a96d2bf278fb2f
Showing with 394 additions and 3 deletions.
  1. +205 −0 auth/auth.go
  2. +93 −0 auth/authapi.go
  3. +52 −0 auth/token.go
  4. +44 −3 mimsrv.go
@@ -0,0 +1,205 @@
// The auth package implements a simple password mechanism to allow
// authentication of API calls.
// We have a database that stores two fields for each user:
// a userid and a cryptword. The cryptword is generated by concatenating
// the userid with the user's password and taking a sha256sum of that.
// For login, the user enters a userid and a password into the client,
// which generates the cryptword. It then gets the current time in seconds
// since the epoch, converts that number to a decimal string, concatenates
// the cryptword with that string, and takes the sha256sum of that, which it
// sends to the server along with the username.

package auth

import (
"bufio"
"crypto/sha256"
"encoding/csv"
"fmt"
"log"
"net/http"
"os"
"strconv"
"syscall"
"time"

"golang.org/x/crypto/ssh/terminal"
)

type Config struct {
Prefix string // The prefix string used for our API calls
PasswordFilePath string // Location of our password database file
MaxClockSkewSeconds int
}

type Handler struct {
ApiHandler http.Handler
config *Config
records [][]string
}

func NewHandler(c *Config) Handler {
h := Handler{config: c}
h.loadPasswordFile()
h.initApiHandler()
initTokens()
return h
}

func (h *Handler) CreatePasswordFile() error {
f, err := os.Open(h.config.PasswordFilePath)
if err == nil || !os.IsNotExist(err) {
return fmt.Errorf("password file already exists at %s", h.config.PasswordFilePath)
}
f, err = os.Create(h.config.PasswordFilePath)
if err != nil {
return fmt.Errorf("error creating new password file at %s: %v", h.config.PasswordFilePath, err)
}
f.Close()
return nil
}

// Read a password from the terminal and pass it to UpdatePassword.
func (h *Handler) UpdateUserPassword(userid string) error {
if !terminal.IsTerminal(syscall.Stdin) {
return fmt.Errorf("updatePassword option requires terminal for input")
}
fmt.Printf("New password: ")
pw, err := terminal.ReadPassword(syscall.Stdin)
fmt.Printf("\n")
if err != nil {
return fmt.Errorf("Error reading new password: %v", err)
}
fmt.Printf("Repeat new password: ")
pw2, err := terminal.ReadPassword(syscall.Stdin)
fmt.Printf("\n")
if err != nil {
return fmt.Errorf("Error reading new password: %v", err)
}
if string(pw2) != string(pw) {
return fmt.Errorf("Passwords did not match")
}
return h.UpdatePassword(userid, string(pw))
}

// Set a password for a user into our password database. We don't save the
// plaintext password, we concatenate the userid with the raw password, take
// the sha256sum of that, and store that in our database.
func (h *Handler) UpdatePassword(userid, password string) error {
err := h.loadPasswordFile()
if err != nil {
return err
}
cryptword := h.generateCryptword(userid, password)
err = h.setCryptword(userid, cryptword)
if err != nil {
return err
}
err = h.savePasswordFile()
if err != nil {
return err
}
return nil
}

func (h *Handler) loadPasswordFile() error {
f, err := os.Open(h.config.PasswordFilePath)
if err != nil {
return fmt.Errorf("error opening password file %s: %v", h.config.PasswordFilePath, err)
}
r := csv.NewReader(bufio.NewReader(f))

records, err := r.ReadAll()
if err != nil {
return fmt.Errorf("error loading password file %s: %v", h.config.PasswordFilePath, err)
}

h.records = records
log.Printf("Number of passwd records: %v\n", len(h.records))
return nil
}

func (h *Handler) savePasswordFile() error {
newFilePath := h.config.PasswordFilePath + ".new"
f, err := os.Create(newFilePath)
if err != nil {
return fmt.Errorf("error creating new password file %s: %v", newFilePath, err)
}
w := csv.NewWriter(bufio.NewWriter(f))
err = w.WriteAll(h.records)
if err != nil {
return fmt.Errorf("error writing new password file %s: %v", newFilePath, err)
}
w.Flush()
f.Close()

backupFilePath := h.config.PasswordFilePath + "~"
err = os.Rename(h.config.PasswordFilePath, backupFilePath)
if err != nil {
return fmt.Errorf("error moving old file to backup path %s: %v", backupFilePath, err)
}
err = os.Rename(newFilePath, h.config.PasswordFilePath)
if err != nil {
return fmt.Errorf("error moving new file %s to become active file: %v", newFilePath, err)
}

return nil
}

func (h *Handler) setCryptword(userid, cryptword string) error {
for r, record := range(h.records) {
if record[0] == userid {
h.records[r][1] = cryptword
return nil
}
}
record := []string{userid, cryptword}
h.records = append(h.records, record)
return nil
}

// Get the encrypted password for the given user from our previously-loaded password file.
func (h *Handler) getCryptword(userid string) string {
for _, record := range(h.records) {
if record[0] == userid {
return record[1]
}
}
return ""
}

func (h *Handler) generateCryptword(userid, password string) string {
return sha256sum(userid + "-" + password)
}

func (h *Handler) generateNonceAtTime(userid string, secondsSinceEpoch int64) string {
cryptword := h.getCryptword(userid)
shaInput := cryptword + "-" + strconv.FormatInt(secondsSinceEpoch, 10)
return sha256sum(shaInput)
}

func (h *Handler) nonceIsValidAtTime(userid, nonce string, secondsSinceEpoch int64) bool {
goodNonce := h.generateNonceAtTime(userid, secondsSinceEpoch)
if nonce == goodNonce {
return true
} else {
log.Printf("nonce %v does not match goodNonce %v", nonce, goodNonce)
return false
}
}

func (h *Handler) nonceIsValidNow(userid, nonce string, seconds int64) bool {
t := time.Now().Unix()
delta := t - seconds
if delta > int64(h.config.MaxClockSkewSeconds) || delta < -int64(h.config.MaxClockSkewSeconds) {
log.Printf("now=%v, client-time=%v, skew is more than max of %v",
t, seconds, h.config.MaxClockSkewSeconds)
return false
}
return h.nonceIsValidAtTime(userid, nonce, seconds)
}

func sha256sum(s string) string {
sum := sha256.Sum256([]byte(s))
return fmt.Sprintf("%x", sum)
}
@@ -0,0 +1,93 @@
package auth

import (
"fmt"
"log"
"net/http"
"strconv"
"time"
)

const (
tokenCookieName = "MIMSRV_TOKEN"
)

func (h *Handler) initApiHandler() {
mux := http.NewServeMux()
mux.HandleFunc(h.apiPrefix("login"), h.login)
mux.HandleFunc(h.apiPrefix("logout"), h.logout)
h.ApiHandler = mux
}

func (h *Handler) RequireAuth(httpHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
token := cookieValue(r, tokenCookieName)
idstr := clientIdString(r)
if isValidToken(token, idstr) {
httpHandler.ServeHTTP(w, r)
} else {
// No token, or token is not valid
http.Error(w, "Invalid token", http.StatusUnauthorized)
}
})
}

func (h *Handler) apiPrefix(s string) string {
return fmt.Sprintf("%s%s/", h.config.Prefix, s)
}

func (h *Handler) login(w http.ResponseWriter, r *http.Request) {
userid := r.FormValue("userid")
nonce := r.FormValue("nonce")
timestr := r.FormValue("time")
seconds, err := strconv.ParseInt(timestr, 10, 64)
if err != nil {
log.Printf("Error converting time string '%s': %v\n", timestr, err)
seconds = 0
}

if h.nonceIsValidNow(userid, nonce, seconds) {
// OK to log in; generate a bearer token and put in a cookie
idstr := clientIdString(r)
token := newToken(userid, idstr)
tokenCookie := &http.Cookie{
Name: tokenCookieName,
Path: "/",
Value: token.Key,
Expires: token.expiry,
HttpOnly: true,
}
http.SetCookie(w, tokenCookie)
} else {
http.Error(w, "Invalid userid or nonce", http.StatusUnauthorized)
return
}

w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "ok"}`))
}

func (h *Handler) logout(w http.ResponseWriter, r *http.Request) {
// Clear our token cookie
tokenCookie := &http.Cookie{
Name: tokenCookieName,
Path: "/",
Value: "",
Expires: time.Now().AddDate(-1, 0, 0),
}
http.SetCookie(w, tokenCookie)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "ok"}`))
}

func clientIdString(r *http.Request) string {
return r.UserAgent()
}

func cookieValue(r *http.Request, cookieName string) string {
cookie, err := r.Cookie(cookieName)
if err != nil {
return ""
}
return cookie.Value
}
@@ -0,0 +1,52 @@
package auth

import (
"fmt"
"math/rand"
"time"
)

const (
tokenExpirationDuration = time.Duration(1) * time.Hour
)

var (
tokens map[string]*Token
)

type Token struct {
Key string
userid string
idstr string
expiry time.Time
}

func initTokens() {
tokens = make(map[string]*Token)
}

func newToken(userid, idstr string) *Token {
token := &Token{
userid: userid,
idstr: idstr,
expiry: time.Now().Add(tokenExpirationDuration),
}
keynum := rand.Intn(1000000)
token.Key = fmt.Sprintf("%06d", keynum)
tokens[token.Key] = token
return token
}

func isValidToken(tokenKey, idstr string) bool {
token := tokens[tokenKey]
if token == nil {
return false
}
if token.idstr != idstr {
return false
}
if time.Now().After(token.expiry) {
return false
}
return true
}

0 comments on commit 44a6029

Please sign in to comment.
You can’t perform that action at this time.