round out authentication

This commit is contained in:
Sky Johnson 2025-08-09 11:27:26 -05:00
parent cec2b12c35
commit 56dca44815
9 changed files with 533 additions and 103 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
# Dragon Knight test/build files # Dragon Knight test/build files
/dk /dk
/dk.db /dk.db
/dk.db-*
/sessions.json

View File

@ -39,7 +39,7 @@ func (am *AuthManager) Authenticate(usernameOrEmail, plainPassword string) (*Use
} }
// Verify password // Verify password
isValid, err := password.Verify(user.Password, plainPassword) isValid, err := password.Verify(plainPassword, user.Password)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -74,6 +74,10 @@ func (am *AuthManager) SessionStats() (total, active int) {
return am.sessionStore.Stats() return am.sessionStore.Stats()
} }
func (am *AuthManager) DB() *database.DB {
return am.db
}
func (am *AuthManager) Close() error { func (am *AuthManager) Close() error {
return am.sessionStore.Close() return am.sessionStore.Close()
} }

View File

@ -5,9 +5,12 @@ import (
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"time"
"dk/internal/auth" "dk/internal/auth"
"dk/internal/router" "dk/internal/router"
"github.com/valyala/fasthttp"
) )
const ( const (
@ -15,6 +18,7 @@ const (
TokenFieldName = "_csrf_token" TokenFieldName = "_csrf_token"
SessionKey = "csrf_token" SessionKey = "csrf_token"
SessionCtxKey = "session" // Same as middleware.SessionKey SessionCtxKey = "session" // Same as middleware.SessionKey
CookieName = "_csrf"
) )
// GetCurrentSession retrieves the session from context (mirrors middleware function) // GetCurrentSession retrieves the session from context (mirrors middleware function)
@ -25,7 +29,7 @@ func GetCurrentSession(ctx router.Ctx) *auth.Session {
return nil return nil
} }
// GenerateToken creates a new CSRF token and stores it in the session // GenerateToken creates a new CSRF token and stores it in the session or cookie
func GenerateToken(ctx router.Ctx, authManager *auth.AuthManager) string { func GenerateToken(ctx router.Ctx, authManager *auth.AuthManager) string {
// Generate cryptographically secure random bytes // Generate cryptographically secure random bytes
tokenBytes := make([]byte, TokenLength) tokenBytes := make([]byte, TokenLength)
@ -36,42 +40,54 @@ func GenerateToken(ctx router.Ctx, authManager *auth.AuthManager) string {
token := base64.URLEncoding.EncodeToString(tokenBytes) token := base64.URLEncoding.EncodeToString(tokenBytes)
// Store token in session if user is authenticated // Store token in session if user is authenticated, otherwise use cookie
if session := GetCurrentSession(ctx); session != nil { if session := GetCurrentSession(ctx); session != nil {
StoreToken(session, token) StoreToken(session, token)
} else {
// Store in cookie for guest users
StoreTokenInCookie(ctx, token)
} }
return token return token
} }
// GetToken retrieves the current CSRF token from session, generating one if needed // GetToken retrieves the current CSRF token from session or cookie, generating one if needed
func GetToken(ctx router.Ctx, authManager *auth.AuthManager) string { func GetToken(ctx router.Ctx, authManager *auth.AuthManager) string {
session := GetCurrentSession(ctx) session := GetCurrentSession(ctx)
if session == nil {
return "" // No session, no CSRF protection needed
}
// Check if token already exists in session if session != nil {
// Authenticated user - check session first
if existingToken := GetStoredToken(session); existingToken != "" { if existingToken := GetStoredToken(session); existingToken != "" {
return existingToken return existingToken
} }
} else {
// Guest user - check cookie first
if existingToken := GetTokenFromCookie(ctx); existingToken != "" {
return existingToken
}
}
// Generate new token if none exists // Generate new token if none exists
return GenerateToken(ctx, authManager) return GenerateToken(ctx, authManager)
} }
// ValidateToken verifies a CSRF token against the stored session token // ValidateToken verifies a CSRF token against the stored session or cookie token
func ValidateToken(ctx router.Ctx, authManager *auth.AuthManager, submittedToken string) bool { func ValidateToken(ctx router.Ctx, authManager *auth.AuthManager, submittedToken string) bool {
if submittedToken == "" { if submittedToken == "" {
return false return false
} }
var storedToken string
session := GetCurrentSession(ctx) session := GetCurrentSession(ctx)
if session == nil {
return false // No session means no CSRF protection if session != nil {
// Authenticated user - get token from session
storedToken = GetStoredToken(session)
} else {
// Guest user - get token from cookie
storedToken = GetTokenFromCookie(ctx)
} }
storedToken := GetStoredToken(session)
if storedToken == "" { if storedToken == "" {
return false // No stored token return false // No stored token
} }
@ -122,7 +138,7 @@ func HiddenField(ctx router.Ctx, authManager *auth.AuthManager) string {
} }
return fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`, return fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`,
TokenFieldName, escapeHTML(token)) TokenFieldName, token)
} }
// TokenMeta generates HTML meta tag for JavaScript access to CSRF token // TokenMeta generates HTML meta tag for JavaScript access to CSRF token
@ -132,16 +148,7 @@ func TokenMeta(ctx router.Ctx, authManager *auth.AuthManager) string {
return "" return ""
} }
return fmt.Sprintf(`<meta name="csrf-token" content="%s">`, escapeHTML(token)) return fmt.Sprintf(`<meta name="csrf-token" content="%s">`, token)
}
// escapeHTML provides basic HTML escaping for token values
func escapeHTML(s string) string {
// Basic HTML escaping - base64 tokens shouldn't need much escaping
// but better safe than sorry
s = fmt.Sprintf("%s", s) // Ensure it's a string
// Base64 URL encoding uses only safe characters, but let's be thorough
return s
} }
// ValidateFormToken is a convenience function to validate CSRF token from form data // ValidateFormToken is a convenience function to validate CSRF token from form data
@ -159,3 +166,22 @@ func ValidateFormToken(ctx router.Ctx, authManager *auth.AuthManager) bool {
return ValidateToken(ctx, authManager, string(tokenBytes)) return ValidateToken(ctx, authManager, string(tokenBytes))
} }
// StoreTokenInCookie stores a CSRF token in a cookie for guest users
func StoreTokenInCookie(ctx router.Ctx, token string) {
cookie := &fasthttp.Cookie{}
cookie.SetKey(CookieName)
cookie.SetValue(token)
cookie.SetHTTPOnly(true)
cookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
cookie.SetSecure(false) // Set to true in production with HTTPS
cookie.SetExpire(time.Now().Add(24 * time.Hour)) // Expire in 24 hours
cookie.SetPath("/")
ctx.Response.Header.SetCookie(cookie)
}
// GetTokenFromCookie retrieves a CSRF token from cookie for guest users
func GetTokenFromCookie(ctx router.Ctx) string {
return string(ctx.Request.Header.Cookie(CookieName))
}

View File

@ -59,10 +59,8 @@ func CSRF(authManager *auth.AuthManager, config ...CSRFConfig) router.Middleware
} }
} }
// Skip CSRF for non-authenticated users (no session) // CSRF protection now works for both authenticated and guest users
if !shouldSkip && !IsAuthenticated(ctx) { // Remove the skip for non-authenticated users
shouldSkip = true
}
if shouldSkip { if shouldSkip {
next(ctx, params) next(ctx, params)

376
internal/routes/auth.go Normal file
View File

@ -0,0 +1,376 @@
package routes
import (
"fmt"
"strings"
"dk/internal/auth"
"dk/internal/csrf"
"dk/internal/middleware"
"dk/internal/password"
"dk/internal/router"
"dk/internal/template"
"dk/internal/users"
"github.com/valyala/fasthttp"
)
// RegisterAuthRoutes sets up authentication routes
func RegisterAuthRoutes(r *router.Router, authManager *auth.AuthManager, templateCache *template.Cache) {
// Guest routes (redirect to dashboard if already authenticated)
guestGroup := r.Group("")
guestGroup.Use(middleware.RequireGuest("/"))
guestGroup.Get("/login", showLogin(authManager, templateCache))
guestGroup.Post("/login", processLogin(authManager, templateCache))
guestGroup.Get("/register", showRegister(authManager, templateCache))
guestGroup.Post("/register", processRegister(authManager, templateCache))
// Authenticated routes
authGroup := r.Group("")
authGroup.Use(middleware.RequireAuth("/login"))
authGroup.Post("/logout", processLogout(authManager))
}
// showLogin displays the login form
func showLogin(authManager *auth.AuthManager, templateCache *template.Cache) router.Handler {
return func(ctx router.Ctx, params []string) {
layoutTmpl, err := templateCache.Load("layout.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
loginTmpl, err := templateCache.Load("auth/login.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
loginFormData := map[string]any{
"csrf_token": csrf.GetToken(ctx, authManager),
"csrf_field": csrf.HiddenField(ctx, authManager),
"error_message": "",
}
loginContent := loginTmpl.RenderNamed(loginFormData)
data := map[string]any{
"title": "Login - Dragon Knight",
"content": loginContent,
"topnav": "",
"leftside": "",
"rightside": "",
"totaltime": middleware.GetRequestTime(ctx),
"numqueries": "0",
"version": "1.0.0",
"build": "dev",
}
layoutTmpl.WriteTo(ctx, data)
}
}
// processLogin handles login form submission
func processLogin(authManager *auth.AuthManager, templateCache *template.Cache) router.Handler {
return func(ctx router.Ctx, params []string) {
// Validate CSRF token
if !csrf.ValidateFormToken(ctx, authManager) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.WriteString("CSRF validation failed")
return
}
// Get form values
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
userPassword := string(ctx.PostArgs().Peek("password"))
// Validate input
if email == "" || userPassword == "" {
showLoginError(ctx, authManager, templateCache, "Email and password are required")
return
}
// Authenticate user
user, err := authManager.Authenticate(email, userPassword)
if err != nil {
showLoginError(ctx, authManager, templateCache, "Invalid email or password")
return
}
// Create session and login
middleware.Login(ctx, authManager, user)
// Redirect to dashboard
ctx.Redirect("/dashboard", fasthttp.StatusFound)
}
}
// showRegister displays the registration form
func showRegister(authManager *auth.AuthManager, templateCache *template.Cache) router.Handler {
return func(ctx router.Ctx, params []string) {
layoutTmpl, err := templateCache.Load("layout.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
registerTmpl, err := templateCache.Load("auth/register.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
registerFormData := map[string]any{
"csrf_token": csrf.GetToken(ctx, authManager),
"csrf_field": csrf.HiddenField(ctx, authManager),
"error_message": "",
"username": "",
"email": "",
}
registerContent := registerTmpl.RenderNamed(registerFormData)
data := map[string]any{
"title": "Register - Dragon Knight",
"content": registerContent,
"topnav": "",
"leftside": "",
"rightside": "",
"totaltime": middleware.GetRequestTime(ctx),
"numqueries": "0",
"version": "1.0.0",
"build": "dev",
}
layoutTmpl.WriteTo(ctx, data)
}
}
// processRegister handles registration form submission
func processRegister(authManager *auth.AuthManager, templateCache *template.Cache) router.Handler {
return func(ctx router.Ctx, params []string) {
// Validate CSRF token
if !csrf.ValidateFormToken(ctx, authManager) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.WriteString("CSRF validation failed")
return
}
// Get form values
username := strings.TrimSpace(string(ctx.PostArgs().Peek("username")))
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
userPassword := string(ctx.PostArgs().Peek("password"))
confirmPassword := string(ctx.PostArgs().Peek("confirm_password"))
// Validate input
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
showRegisterError(ctx, authManager, templateCache, err.Error(), username, email)
return
}
// Check if username already exists
if _, err := users.GetByUsername(authManager.DB(), username); err == nil {
showRegisterError(ctx, authManager, templateCache, "Username already exists", username, email)
return
}
// Check if email already exists
if _, err := users.GetByEmail(authManager.DB(), email); err == nil {
showRegisterError(ctx, authManager, templateCache, "Email already registered", username, email)
return
}
// Hash password
hashedPassword, err := password.Hash(userPassword)
if err != nil {
showRegisterError(ctx, authManager, templateCache, "Failed to process password", username, email)
return
}
// Create user (this is a simplified approach - in a real app you'd use a proper user creation function)
user := &users.User{
Username: username,
Email: email,
Password: hashedPassword,
Verified: 1, // Auto-verify for now
Auth: 1, // Enabled
}
// Insert into database
if err := createUser(authManager, user); err != nil {
showRegisterError(ctx, authManager, templateCache, "Failed to create account", username, email)
return
}
// Auto-login after registration
authUser := &auth.User{
ID: user.ID,
Username: user.Username,
Email: user.Email,
}
middleware.Login(ctx, authManager, authUser)
ctx.Redirect("/", fasthttp.StatusFound)
}
}
// processLogout handles logout
func processLogout(authManager *auth.AuthManager) router.Handler {
return func(ctx router.Ctx, params []string) {
// Validate CSRF token
if !csrf.ValidateFormToken(ctx, authManager) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.WriteString("CSRF validation failed")
return
}
middleware.Logout(ctx, authManager)
ctx.Redirect("/", fasthttp.StatusFound)
}
}
// Helper functions
func showLoginError(ctx router.Ctx, authManager *auth.AuthManager, templateCache *template.Cache, errorMsg string) {
layoutTmpl, err := templateCache.Load("layout.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
loginTmpl, err := templateCache.Load("auth/login.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
var errorHTML string
if errorMsg != "" {
errorHTML = fmt.Sprintf(`<div style="color: red; margin-bottom: 1rem;">%s</div>`, errorMsg)
}
loginFormData := map[string]any{
"csrf_token": csrf.GetToken(ctx, authManager),
"csrf_field": csrf.HiddenField(ctx, authManager),
"error_message": errorHTML,
}
loginContent := loginTmpl.RenderNamed(loginFormData)
data := map[string]any{
"title": "Login - Dragon Knight",
"content": loginContent,
"topnav": "",
"leftside": "",
"rightside": "",
"totaltime": middleware.GetRequestTime(ctx),
"numqueries": "0",
"version": "1.0.0",
"build": "dev",
}
ctx.SetStatusCode(fasthttp.StatusBadRequest)
layoutTmpl.WriteTo(ctx, data)
}
func showRegisterError(ctx router.Ctx, authManager *auth.AuthManager, templateCache *template.Cache, errorMsg, username, email string) {
layoutTmpl, err := templateCache.Load("layout.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
registerTmpl, err := templateCache.Load("auth/register.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
var errorHTML string
if errorMsg != "" {
errorHTML = fmt.Sprintf(`<div style="color: red; margin-bottom: 1rem;">%s</div>`, errorMsg)
}
registerFormData := map[string]any{
"csrf_token": csrf.GetToken(ctx, authManager),
"csrf_field": csrf.HiddenField(ctx, authManager),
"error_message": errorHTML,
"username": username,
"email": email,
}
registerContent := registerTmpl.RenderNamed(registerFormData)
data := map[string]any{
"title": "Register - Dragon Knight",
"content": registerContent,
"topnav": "",
"leftside": "",
"rightside": "",
"totaltime": middleware.GetRequestTime(ctx),
"numqueries": "0",
"version": "1.0.0",
"build": "dev",
}
ctx.SetStatusCode(fasthttp.StatusBadRequest)
layoutTmpl.WriteTo(ctx, data)
}
func validateRegistration(username, email, password, confirmPassword string) error {
if username == "" {
return fmt.Errorf("username is required")
}
if len(username) < 3 {
return fmt.Errorf("username must be at least 3 characters")
}
if email == "" {
return fmt.Errorf("email is required")
}
if !strings.Contains(email, "@") {
return fmt.Errorf("invalid email address")
}
if password == "" {
return fmt.Errorf("password is required")
}
if len(password) < 6 {
return fmt.Errorf("password must be at least 6 characters")
}
if password != confirmPassword {
return fmt.Errorf("passwords do not match")
}
return nil
}
// createUser inserts a new user into the database
// This is a simplified version - in a real app you'd have a proper users.Create function
func createUser(authManager *auth.AuthManager, user *users.User) error {
db := authManager.DB()
query := `INSERT INTO users (username, password, email, verified, auth) VALUES (?, ?, ?, ?, ?)`
err := db.Exec(query, user.Username, user.Password, user.Email, user.Verified, user.Auth)
if err != nil {
return fmt.Errorf("failed to insert user: %w", err)
}
// Get the user ID (simplified - in real app you'd return it from insert)
createdUser, err := users.GetByUsername(db, user.Username)
if err != nil {
return fmt.Errorf("failed to get created user: %w", err)
}
user.ID = createdUser.ID
return nil
}

13
internal/routes/doc.go Normal file
View File

@ -0,0 +1,13 @@
// Package routes organizes HTTP route handlers for the Dragon Knight application.
// Routes are organized by feature area in separate packages to maintain clean
// separation of concerns and make the codebase more maintainable.
//
// # Structure
//
// - auth/ - Authentication routes (login, register, logout)
// - api/ - API endpoints
// - web/ - Web interface routes
//
// Each route package should provide a Setup function that registers its routes
// with the router and returns any necessary dependencies or configuration.
package routes

View File

@ -12,6 +12,7 @@ import (
"dk/internal/database" "dk/internal/database"
"dk/internal/middleware" "dk/internal/middleware"
"dk/internal/router" "dk/internal/router"
"dk/internal/routes"
"dk/internal/template" "dk/internal/template"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@ -26,7 +27,7 @@ func Start(port string) error {
templateCache := template.NewCache(cwd) templateCache := template.NewCache(cwd)
// Initialize database // Initialize database
db, err := database.Open("dk.sqlite") db, err := database.Open("dk.db")
if err != nil { if err != nil {
return fmt.Errorf("failed to open database: %w", err) return fmt.Errorf("failed to open database: %w", err)
} }
@ -34,7 +35,7 @@ func Start(port string) error {
// Initialize authentication manager // Initialize authentication manager
authManager := auth.NewAuthManager(db, "sessions.json") authManager := auth.NewAuthManager(db, "sessions.json")
defer authManager.Close() // Don't defer Close() here - we'll handle it in shutdown
// Initialize router // Initialize router
r := router.New() r := router.New()
@ -42,8 +43,40 @@ func Start(port string) error {
// Add middleware // Add middleware
r.Use(middleware.Timing()) r.Use(middleware.Timing())
r.Use(middleware.Auth(authManager)) r.Use(middleware.Auth(authManager))
r.Use(middleware.CSRF(authManager))
// Hello world endpoint // Setup route handlers
routes.RegisterAuthRoutes(r, authManager, templateCache)
// Dashboard (protected route)
r.WithMiddleware(middleware.RequireAuth("/login")).Get("/dashboard", func(ctx router.Ctx, params []string) {
tmpl, err := templateCache.Load("layout.html")
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
fmt.Fprintf(ctx, "Template error: %v", err)
return
}
currentUser := middleware.GetCurrentUser(ctx)
totalSessions, activeSessions := authManager.SessionStats()
data := map[string]any{
"title": "Dashboard - Dragon Knight",
"content": fmt.Sprintf("Welcome back, %s!", currentUser.Username),
"totaltime": middleware.GetRequestTime(ctx),
"numqueries": "0",
"version": "1.0.0",
"build": "dev",
"total_sessions": totalSessions,
"active_sessions": activeSessions,
"authenticated": true,
"username": currentUser.Username,
}
tmpl.WriteTo(ctx, data)
})
// Hello world endpoint (public)
r.Get("/", func(ctx router.Ctx, params []string) { r.Get("/", func(ctx router.Ctx, params []string) {
tmpl, err := templateCache.Load("layout.html") tmpl, err := templateCache.Load("layout.html")
if err != nil { if err != nil {
@ -126,15 +159,18 @@ func Start(port string) error {
} }
}() }()
// Block until we receive a signal // Wait for interrupt signal
<-c <-c
log.Println("Shutting down server...") log.Println("Received shutdown signal, shutting down gracefully...")
// Shutdown server gracefully // Save sessions before shutdown
if err := server.Shutdown(); err != nil { log.Println("Saving sessions...")
log.Printf("Server shutdown error: %v", err) if err := authManager.Close(); err != nil {
log.Printf("Error saving sessions: %v", err)
} }
// FastHTTP doesn't have a graceful Shutdown method like net/http
// We just let the server stop naturally when the main function exits
log.Println("Server stopped") log.Println("Server stopped")
return nil return nil
} }

View File

@ -1,30 +1,21 @@
{flashhtml} {error_message}
<form action="/login" method="post"> <form action="/login" method="post">
{csrf} {csrf_field}
<table width="75%"> <table width="75%">
<tr> <tr>
<td width="30%">Username:</td> <td width="30%">Email/Username:</td>
<td><input type="text" size="30" name="username"></td> <td><input type="text" size="30" name="email" required></td>
</tr> </tr>
<tr> <tr>
<td>Password:</td> <td>Password:</td>
<td><input type="password" size="30" name="password"></td> <td><input type="password" size="30" name="password" required></td>
</tr>
<tr>
<td>Remember me?</td>
<td><input type="checkbox" name="rememberme" value="yes"> Yes</td>
</tr> </tr>
<tr> <tr>
<td colspan="2"><input type="submit" name="submit" value="Log In"></td> <td colspan="2"><input type="submit" name="submit" value="Log In"></td>
</tr> </tr>
<tr> <tr>
<td colspan="2"> <td colspan="2">
Checking the "Remember Me" option will store your login information in a cookie
so you don't have to enter it next time you get online.
<br><br>
Want to play? You gotta <a href="/register">register your own character.</a> Want to play? You gotta <a href="/register">register your own character.</a>
<br><br> <br><br>

View File

@ -1,12 +1,12 @@
{flashhtml} {error_message}
<form action="/register" method="post"> <form action="/register" method="post">
{csrf} {csrf_field}
<table width="80%"> <table width="80%">
<tr> <tr>
<td width="20%">Username:</td> <td width="20%">Username:</td>
<td> <td>
<input type="text" name="username" size="30" maxlength="30"> <input type="text" name="username" size="30" maxlength="30" value="{username}" required>
<br> <br>
Usernames must be 30 alphanumeric characters or less. Usernames must be 30 alphanumeric characters or less.
<br><br><br> <br><br><br>
@ -14,45 +14,29 @@
</tr> </tr>
<tr> <tr>
<td>Password:</td> <td>Password:</td>
<td><input type="password" name="password1" size="30" maxlength="10"></td> <td><input type="password" name="password" size="30" required></td>
</tr> </tr>
<tr> <tr>
<td>Verify Password:</td> <td>Verify Password:</td>
<td> <td>
<input type="password" name="password2" size="30" maxlength="10"> <input type="password" name="confirm_password" size="30" required>
<br> <br>
Passwords must be 10 alphanumeric characters or less. Passwords must be at least 6 characters.
<br><br><br> <br><br><br>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Email Address:</td> <td>Email Address:</td>
<td><input type="email" name="email1" size="30" maxlength="100"></td>
</tr>
<tr>
<td>Verify Email:</td>
<td> <td>
<input type="text" name="email2" size="30" maxlength="100"> <input type="email" name="email" size="30" maxlength="100" value="{email}" required>
{verifytext} <br>
A valid email address is required.
<br><br><br> <br><br><br>
</td> </td>
</tr> </tr>
<tr>
<td>Character Class:</td>
<td>
<select name="charclass">
<option value="1">{class1name}</option>
<option value="2">{class2name}</option>
<option value="3">{class3name}</option>
</select>
</td>
</tr>
<tr>
<td colspan="2">See <a href="/help">Help</a> for more information about character classes.<br><br></td>
</tr>
<tr> <tr>
<td colspan="2"> <td colspan="2">
<input type="submit" name="submit" value="Submit"> <input type="submit" name="submit" value="Register">
<input type="reset" name="reset" value="Reset"> <input type="reset" name="reset" value="Reset">
</td> </td>
</tr> </tr>