diff --git a/README.md b/README.md
index 0e9d50615..c9dd9bb98 100644
--- a/README.md
+++ b/README.md
@@ -83,15 +83,10 @@ This will generate the `navidrome` binary in the project's root folder. Start th
 ```
 The server should start listening for requests on the default port __4533__
 
-### First time password
-The first time you start the app it will create a new user "admin" with a random password. 
-Check the logs for a line like this:
-```
-Creating initial user. Please change the password!  password=XXXXXX user=admin
-```
+### Running for the first time
 
-You can change this password using the UI. Just browse to http://localhost:4533/app#/user 
-and login with this temporary password.  
+After starting Navidrome for the first time, go to http://localhost:4533. It will ask you to create your first admin 
+user.
 
 ## Screenshots
 
diff --git a/consts/consts.go b/consts/consts.go
index ce14187b4..6cf3e37f9 100644
--- a/consts/consts.go
+++ b/consts/consts.go
@@ -7,11 +7,10 @@ const (
 	InitialSetupFlagKey = "InitialSetup"
 
 	JWTSecretKey       = "JWTSecret"
-	JWTIssuer          = "Navidrome"
+	JWTIssuer          = "ND"
 	JWTTokenExpiration = 30 * time.Minute
 
 	InitialUserName = "admin"
-	InitialName     = "Admin"
 
 	UIAssetsLocalPath = "ui/build"
 )
diff --git a/persistence/user_repository.go b/persistence/user_repository.go
index ac768cb1a..9187dccc9 100644
--- a/persistence/user_repository.go
+++ b/persistence/user_repository.go
@@ -63,7 +63,7 @@ func (r *userRepository) Put(u *model.User) error {
 
 func (r *userRepository) FindByUsername(username string) (*model.User, error) {
 	tu := user{}
-	err := r.ormer.QueryTable(user{}).Filter("user_name", username).One(&tu)
+	err := r.ormer.QueryTable(user{}).Filter("user_name__iexact", username).One(&tu)
 	if err == orm.ErrNoRows {
 		return nil, model.ErrNotFound
 	}
diff --git a/server/app/app.go b/server/app/app.go
index ca4504aea..fe9f49114 100644
--- a/server/app/app.go
+++ b/server/app/app.go
@@ -43,11 +43,12 @@ func (app *Router) routes() http.Handler {
 	r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"response":"pong"}`)) })
 
 	r.Post("/login", Login(app.ds))
+	r.Post("/createAdmin", CreateAdmin(app.ds))
 
 	r.Route("/api", func(r chi.Router) {
 		if !conf.Server.DevDisableAuthentication {
 			r.Use(jwtauth.Verifier(TokenAuth))
-			r.Use(Authenticator)
+			r.Use(Authenticator(app.ds))
 		}
 		app.R(r, "/user", model.User{})
 		app.R(r, "/song", model.MediaFile{})
diff --git a/server/app/auth.go b/server/app/auth.go
index dfb85cc13..c70a93c24 100644
--- a/server/app/auth.go
+++ b/server/app/auth.go
@@ -3,64 +3,128 @@ package app
 import (
 	"context"
 	"encoding/json"
+	"errors"
 	"net/http"
 	"strings"
 	"sync"
 	"time"
 
 	"github.com/deluan/navidrome/consts"
+	"github.com/deluan/navidrome/log"
 	"github.com/deluan/navidrome/model"
 	"github.com/deluan/rest"
 	"github.com/dgrijalva/jwt-go"
 	"github.com/go-chi/jwtauth"
-	log "github.com/sirupsen/logrus"
+	"github.com/google/uuid"
 )
 
 var (
-	once      sync.Once
-	jwtSecret []byte
-	TokenAuth *jwtauth.JWTAuth
+	once         sync.Once
+	jwtSecret    []byte
+	TokenAuth    *jwtauth.JWTAuth
+	ErrFirstTime = errors.New("no users created")
 )
 
 func Login(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) {
 	initTokenAuth(ds)
 
 	return func(w http.ResponseWriter, r *http.Request) {
-		data := make(map[string]string)
-		decoder := json.NewDecoder(r.Body)
-		if err := decoder.Decode(&data); err != nil {
-			log.Errorf("parsing request body: %#v", err)
-			rest.RespondWithError(w, http.StatusUnprocessableEntity, "Invalid request payload")
-			return
-		}
-		username := data["username"]
-		password := data["password"]
-
-		user, err := validateLogin(ds.User(), username, password)
+		username, password, err := getCredentialsFromBody(r)
 		if err != nil {
-			rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authentication user. Please try again")
-			return
-		}
-		if user == nil {
-			log.Warnf("Unsuccessful login: '%s', request: %v", username, r.Header)
-			rest.RespondWithError(w, http.StatusUnauthorized, "Invalid username or password")
+			log.Error(r, "Parsing request body", err)
+			rest.RespondWithError(w, http.StatusUnprocessableEntity, err.Error())
 			return
 		}
 
-		tokenString, err := createToken(user)
-		if err != nil {
-			rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authenticating user. Please try again")
-		}
-		rest.RespondWithJSON(w, http.StatusOK,
-			map[string]interface{}{
-				"message":  "User '" + username + "' authenticated successfully",
-				"token":    tokenString,
-				"name":     strings.Title(user.Name),
-				"username": username,
-			})
+		handleLogin(ds, username, password, w, r)
 	}
 }
 
+func handleLogin(ds model.DataStore, username string, password string, w http.ResponseWriter, r *http.Request) {
+	user, err := validateLogin(ds.User(), username, password)
+	if err != nil {
+		rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authentication user. Please try again")
+		return
+	}
+	if user == nil {
+		log.Warn(r, "Unsuccessful login", "username", username, "request", r.Header)
+		rest.RespondWithError(w, http.StatusUnauthorized, "Invalid username or password")
+		return
+	}
+
+	tokenString, err := createToken(user)
+	if err != nil {
+		rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authenticating user. Please try again")
+		return
+	}
+	rest.RespondWithJSON(w, http.StatusOK,
+		map[string]interface{}{
+			"message":  "User '" + username + "' authenticated successfully",
+			"token":    tokenString,
+			"name":     user.Name,
+			"username": username,
+		})
+}
+
+func getCredentialsFromBody(r *http.Request) (username string, password string, err error) {
+	data := make(map[string]string)
+	decoder := json.NewDecoder(r.Body)
+	if err = decoder.Decode(&data); err != nil {
+		log.Error(r, "parsing request body", err)
+		err = errors.New("Invalid request payload")
+		return
+	}
+	username = data["username"]
+	password = data["password"]
+	return username, password, nil
+}
+
+func CreateAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) {
+	initTokenAuth(ds)
+
+	return func(w http.ResponseWriter, r *http.Request) {
+		username, password, err := getCredentialsFromBody(r)
+		if err != nil {
+			log.Error(r, "parsing request body", err)
+			rest.RespondWithError(w, http.StatusUnprocessableEntity, err.Error())
+			return
+		}
+		c, err := ds.User().CountAll()
+		if err != nil {
+			rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
+			return
+		}
+		if c > 0 {
+			rest.RespondWithError(w, http.StatusForbidden, "Cannot create another first admin")
+			return
+		}
+		err = createDefaultUser(ds, username, password)
+		if err != nil {
+			rest.RespondWithError(w, http.StatusInternalServerError, err.Error())
+			return
+		}
+		handleLogin(ds, username, password, w, r)
+	}
+}
+
+func createDefaultUser(ds model.DataStore, username, password string) error {
+	id, _ := uuid.NewRandom()
+	log.Warn("Creating initial user", "user", consts.InitialUserName)
+	initialUser := model.User{
+		ID:       id.String(),
+		UserName: username,
+		Name:     strings.Title(username),
+		Email:    "",
+		Password: password,
+		IsAdmin:  true,
+	}
+	err := ds.User().Put(&initialUser)
+	if err != nil {
+		log.Error("Could not create initial user", "user", initialUser, err)
+	}
+	return nil
+}
+
 func initTokenAuth(ds model.DataStore) {
 	once.Do(func() {
 		secret, err := ds.Property().DefaultGet(consts.JWTSecretKey, "not so secret")
@@ -117,31 +181,50 @@ func userFrom(claims jwt.MapClaims) *model.User {
 	return user
 }
 
-func Authenticator(next http.Handler) http.Handler {
-	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		token, _, err := jwtauth.FromContext(r.Context())
+func getToken(ds model.DataStore, ctx context.Context) (*jwt.Token, error) {
+	token, claims, err := jwtauth.FromContext(ctx)
 
-		if err != nil {
-			rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
-			return
-		}
+	valid := err == nil && token != nil && token.Valid
+	valid = valid && claims["sub"] != nil
+	if valid {
+		return token, nil
+	}
 
-		if token == nil || !token.Valid {
-			rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
-			return
-		}
-
-		claims := token.Claims.(jwt.MapClaims)
-
-		newCtx := context.WithValue(r.Context(), "loggedUser", userFrom(claims))
-		newTokenString, err := touchToken(token)
-		if err != nil {
-			log.Errorf("signing new token: %v", err)
-			rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
-			return
-		}
-
-		w.Header().Set("Authorization", newTokenString)
-		next.ServeHTTP(w, r.WithContext(newCtx))
-	})
+	c, err := ds.User().CountAll()
+	firstTime := c == 0 && err == nil
+	if firstTime {
+		return nil, ErrFirstTime
+	}
+	return nil, errors.New("invalid authentication")
+}
+
+func Authenticator(ds model.DataStore) func(next http.Handler) http.Handler {
+	initTokenAuth(ds)
+
+	return func(next http.Handler) http.Handler {
+		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			token, err := getToken(ds, r.Context())
+			if err == ErrFirstTime {
+				rest.RespondWithJSON(w, http.StatusUnauthorized, map[string]string{"message": ErrFirstTime.Error()})
+				return
+			}
+			if err != nil {
+				rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
+				return
+			}
+
+			claims := token.Claims.(jwt.MapClaims)
+
+			newCtx := context.WithValue(r.Context(), "loggedUser", userFrom(claims))
+			newTokenString, err := touchToken(token)
+			if err != nil {
+				log.Error(r, "signing new token", err)
+				rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
+				return
+			}
+
+			w.Header().Set("Authorization", newTokenString)
+			next.ServeHTTP(w, r.WithContext(newCtx))
+		})
+	}
 }
diff --git a/server/initial_setup.go b/server/initial_setup.go
index 043f8eee9..d1b47aa0a 100644
--- a/server/initial_setup.go
+++ b/server/initial_setup.go
@@ -1,10 +1,8 @@
 package server
 
 import (
-	"fmt"
 	"time"
 
-	"github.com/deluan/navidrome/conf"
 	"github.com/deluan/navidrome/consts"
 	"github.com/deluan/navidrome/log"
 	"github.com/deluan/navidrome/model"
@@ -18,9 +16,6 @@ func initialSetup(ds model.DataStore) {
 			return nil
 		}
 		log.Warn("Running initial setup")
-		if err = createDefaultUser(ds); err != nil {
-			return err
-		}
 		if err = createJWTSecret(ds); err != nil {
 			return err
 		}
@@ -43,32 +38,3 @@ func createJWTSecret(ds model.DataStore) error {
 	}
 	return err
 }
-
-func createDefaultUser(ds model.DataStore) error {
-	c, err := ds.User().CountAll()
-	if err != nil {
-		panic(fmt.Sprintf("Could not access User table: %s", err))
-	}
-	if c == 0 {
-		id, _ := uuid.NewRandom()
-		random, _ := uuid.NewRandom()
-		initialPassword := random.String()
-		if conf.Server.DevInitialPassword != "" {
-			initialPassword = conf.Server.DevInitialPassword
-		}
-		log.Warn("Creating initial user. Please change the password!", "user", consts.InitialUserName, "password", initialPassword)
-		initialUser := model.User{
-			ID:       id.String(),
-			UserName: consts.InitialUserName,
-			Name:     consts.InitialName,
-			Email:    "",
-			Password: initialPassword,
-			IsAdmin:  true,
-		}
-		err := ds.User().Put(&initialUser)
-		if err != nil {
-			log.Error("Could not create initial user", "user", initialUser, err)
-		}
-	}
-	return err
-}
diff --git a/ui/src/authProvider.js b/ui/src/authProvider.js
index 0a9c84ee5..5492aa7af 100644
--- a/ui/src/authProvider.js
+++ b/ui/src/authProvider.js
@@ -2,7 +2,11 @@ import jwtDecode from 'jwt-decode'
 
 const authProvider = {
   login: ({ username, password }) => {
-    const request = new Request('/app/login', {
+    let url = '/app/login'
+    if (localStorage.getItem('initialAccountCreation')) {
+      url = '/app/createAdmin'
+    }
+    const request = new Request(url, {
       method: 'POST',
       body: JSON.stringify({ username, password }),
       headers: new Headers({ 'Content-Type': 'application/json' })
@@ -17,6 +21,7 @@ const authProvider = {
       .then((response) => {
         // Validate token
         jwtDecode(response.token)
+        localStorage.removeItem('initialAccountCreation')
         localStorage.setItem('token', response.token)
         localStorage.setItem('name', response.name)
         localStorage.setItem('username', response.username)
@@ -39,19 +44,14 @@ const authProvider = {
     return Promise.resolve()
   },
 
-  checkAuth: () => {
-    try {
-      const expireTime = jwtDecode(localStorage.getItem('token')).exp * 1000
-      const now = new Date().getTime()
-      return now < expireTime ? Promise.resolve() : Promise.reject()
-    } catch (e) {
-      return Promise.reject()
-    }
-  },
+  checkAuth: () =>
+    localStorage.getItem('token') ? Promise.resolve() : Promise.reject(),
 
   checkError: (error) => {
-    const { status } = error
-    // TODO Remove 403?
+    const { status, message } = error
+    if (message === 'no users created') {
+      localStorage.setItem('initialAccountCreation', 'true')
+    }
     if (status === 401 || status === 403) {
       removeItems()
       return Promise.reject()
diff --git a/ui/src/dataProvider.js b/ui/src/dataProvider.js
index d50225ea4..4b0280a04 100644
--- a/ui/src/dataProvider.js
+++ b/ui/src/dataProvider.js
@@ -13,6 +13,7 @@ const httpClient = (url, options = {}) => {
     const token = response.headers.get('authorization')
     if (token) {
       localStorage.setItem('token', token)
+      localStorage.removeItem('initialAccountCreation')
     }
     return response
   })
diff --git a/ui/src/layout/Login.js b/ui/src/layout/Login.js
index 75f6a35bf..67fe5a8bb 100644
--- a/ui/src/layout/Login.js
+++ b/ui/src/layout/Login.js
@@ -71,40 +71,9 @@ const renderInput = ({
   />
 )
 
-const Login = ({ location }) => {
-  const [loading, setLoading] = useState(false)
+const FormLogin = ({ loading, handleSubmit, validate }) => {
   const translate = useTranslate()
   const classes = useStyles()
-  const notify = useNotify()
-  const login = useLogin()
-
-  const handleSubmit = (auth) => {
-    setLoading(true)
-    login(auth, location.state ? location.state.nextPathname : '/').catch(
-      (error) => {
-        setLoading(false)
-        notify(
-          typeof error === 'string'
-            ? error
-            : typeof error === 'undefined' || !error.message
-            ? 'ra.auth.sign_in_error'
-            : error.message,
-          'warning'
-        )
-      }
-    )
-  }
-
-  const validate = (values) => {
-    const errors = {}
-    if (!values.username) {
-      errors.username = translate('ra.validation.required')
-    }
-    if (!values.password) {
-      errors.password = translate('ra.validation.required')
-    }
-    return errors
-  }
 
   return (
     <Form
@@ -162,6 +131,146 @@ const Login = ({ location }) => {
   )
 }
 
+const FormSignUp = ({ loading, handleSubmit, validate }) => {
+  const translate = useTranslate()
+  const classes = useStyles()
+
+  return (
+    <Form
+      onSubmit={handleSubmit}
+      validate={validate}
+      render={({ handleSubmit }) => (
+        <form onSubmit={handleSubmit} noValidate>
+          <div className={classes.main}>
+            <Card className={classes.card}>
+              <div className={classes.avatar}>
+                <Avatar className={classes.icon}>
+                  <LockIcon />
+                </Avatar>
+              </div>
+              <div className={classes.systemName}>
+                Thanks for installing Navidrome!
+              </div>
+              <div className={classes.systemName}>
+                To start, create an admin user
+              </div>
+              <div className={classes.form}>
+                <div className={classes.input}>
+                  <Field
+                    autoFocus
+                    name="username"
+                    component={renderInput}
+                    label={'Admin Username'}
+                    disabled={loading}
+                  />
+                </div>
+                <div className={classes.input}>
+                  <Field
+                    name="password"
+                    component={renderInput}
+                    label={translate('ra.auth.password')}
+                    type="password"
+                    disabled={loading}
+                  />
+                </div>
+                <div className={classes.input}>
+                  <Field
+                    name="confirmPassword"
+                    component={renderInput}
+                    label={'Confirm Password'}
+                    type="password"
+                    disabled={loading}
+                  />
+                </div>
+              </div>
+              <CardActions className={classes.actions}>
+                <Button
+                  variant="contained"
+                  type="submit"
+                  color="primary"
+                  disabled={loading}
+                  className={classes.button}
+                  fullWidth
+                >
+                  {loading && <CircularProgress size={25} thickness={2} />}
+                  {translate('Create Admin')}
+                </Button>
+              </CardActions>
+            </Card>
+            <Notification />
+          </div>
+        </form>
+      )}
+    />
+  )
+}
+const Login = ({ location }) => {
+  const [loading, setLoading] = useState(false)
+  const translate = useTranslate()
+  const notify = useNotify()
+  const login = useLogin()
+
+  const handleSubmit = (auth) => {
+    setLoading(true)
+    login(auth, location.state ? location.state.nextPathname : '/').catch(
+      (error) => {
+        setLoading(false)
+        notify(
+          typeof error === 'string'
+            ? error
+            : typeof error === 'undefined' || !error.message
+            ? 'ra.auth.sign_in_error'
+            : error.message,
+          'warning'
+        )
+      }
+    )
+  }
+
+  const validateLogin = (values) => {
+    const errors = {}
+    if (!values.username) {
+      errors.username = translate('ra.validation.required')
+    }
+    if (!values.password) {
+      errors.password = translate('ra.validation.required')
+    }
+    return errors
+  }
+
+  const validateSignup = (values) => {
+    const errors = validateLogin(values)
+    const regex = /^\w+$/g
+    if (values.username && !values.username.match(regex)) {
+      errors.username = translate('Please only use letter and numbers')
+    }
+    if (!values.confirmPassword) {
+      errors.confirmPassword = translate('ra.validation.required')
+    }
+    if (values.confirmPassword !== values.password) {
+      errors.confirmPassword = 'Password does not match'
+    }
+    return errors
+  }
+
+  if (localStorage.getItem('initialAccountCreation') === 'true') {
+    return (
+      <FormSignUp
+        handleSubmit={handleSubmit}
+        validate={validateSignup}
+        loading={loading}
+      />
+    )
+  }
+  return (
+    <FormLogin
+      handleSubmit={handleSubmit}
+      validate={validateLogin}
+      loading={loading}
+    />
+  )
+}
+
 Login.propTypes = {
   authProvider: PropTypes.func,
   previousRoute: PropTypes.string