Use secure cookies to store session identifier instead of localstorage

This commit is contained in:
nemunaire 2020-05-12 18:17:14 +02:00
parent ad7dde0d0a
commit 5461823525
4 changed files with 155 additions and 98 deletions

View File

@ -34,7 +34,6 @@ package api
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@ -138,14 +137,28 @@ func apiAuthHandler(f func(*config.Options, *happydns.User, httprouter.Params, i
return
}
if flds := strings.Fields(r.Header.Get("Authorization")); len(flds) != 2 || flds[0] != "Bearer" {
var sessionid []byte
if cookie, err := r.Cookie("happydns_session"); err == nil {
if sessionid, err = base64.StdEncoding.DecodeString(cookie.Value); err != nil {
APIErrorResponse{
err: fmt.Errorf("Unable to authenticate request due to invalid cookie value: %w", err),
status: http.StatusUnauthorized,
}.WriteResponse(w)
return
}
} else if flds := strings.Fields(r.Header.Get("Authorization")); len(flds) == 2 && flds[0] == "Bearer" {
if sessionid, err = base64.StdEncoding.DecodeString(flds[1]); err != nil {
APIErrorResponse{
err: fmt.Errorf("Unable to authenticate request due to invalid Authorization header value: %w", err),
status: http.StatusUnauthorized,
}.WriteResponse(w)
}
}
if sessionid == nil || len(sessionid) == 0 {
APIErrorResponse{
err: errors.New("Authorization required"),
status: http.StatusUnauthorized,
}.WriteResponse(w)
} else if sessionid, err := base64.StdEncoding.DecodeString(flds[1]); err != nil {
APIErrorResponse{
err: err,
err: fmt.Errorf("Authorization required"),
status: http.StatusUnauthorized,
}.WriteResponse(w)
} else if session, err := storage.MainStore.GetSession(sessionid); err != nil {

View File

@ -32,9 +32,11 @@
package api
import (
"encoding/base64"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"time"
@ -48,10 +50,11 @@ import (
var AuthFunc = checkAuth
func init() {
router.GET("/api/users/auth", apiAuthHandler(validateAuthToken))
router.POST("/api/users/auth", apiHandler(func(_ *config.Options, ps httprouter.Params, b io.Reader) Response {
return AuthFunc(ps, b)
router.GET("/api/users/auth", apiAuthHandler(displayAuthToken))
router.POST("/api/users/auth", apiHandler(func(opts *config.Options, ps httprouter.Params, b io.Reader) Response {
return AuthFunc(opts, ps, b)
}))
router.POST("/api/users/auth/logout", apiHandler(logout))
}
type DisplayUser struct {
@ -60,13 +63,78 @@ type DisplayUser struct {
RegistrationTime *time.Time `json:"registration_time,omitempty"`
}
func validateAuthToken(_ *config.Options, u *happydns.User, _ httprouter.Params, _ io.Reader) Response {
func currentUser(u *happydns.User) *DisplayUser {
return &DisplayUser{
Id: u.Id,
Email: u.Email,
RegistrationTime: u.RegistrationTime,
}
}
func displayAuthToken(_ *config.Options, u *happydns.User, _ httprouter.Params, _ io.Reader) Response {
return APIResponse{
response: &DisplayUser{
Id: u.Id,
Email: u.Email,
RegistrationTime: u.RegistrationTime,
},
response: currentUser(u),
}
}
func completeAuth(opts *config.Options, email string, service string) Response {
var usr *happydns.User
var err error
if !storage.MainStore.UserExists(email) {
now := time.Now()
usr = &happydns.User{
Email: email,
RegistrationTime: &now,
}
if err = storage.MainStore.CreateUser(usr); err != nil {
return APIErrorResponse{
err: err,
}
}
} else if usr, err = storage.MainStore.GetUserByEmail(email); err != nil {
return APIErrorResponse{
err: err,
}
}
log.Printf("New user logged as %q\n", usr.Email)
var session *happydns.Session
if session, err = happydns.NewSession(usr); err != nil {
return APIErrorResponse{
err: err,
}
} else if err = storage.MainStore.CreateSession(session); err != nil {
return APIErrorResponse{
err: err,
}
}
return APIResponse{
response: currentUser(usr),
cookies: []*http.Cookie{&http.Cookie{
Name: "happydns_session",
Value: base64.StdEncoding.EncodeToString(session.Id),
Path: opts.BaseURL + "/",
Expires: time.Now().Add(30 * 24 * time.Hour),
Secure: opts.DevProxy == "",
HttpOnly: true,
}},
}
}
func logout(opts *config.Options, _ httprouter.Params, body io.Reader) Response {
return APIResponse{
response: true,
cookies: []*http.Cookie{&http.Cookie{
Name: "happydns_session",
Value: "",
Path: opts.BaseURL + "/",
Expires: time.Unix(0, 0),
Secure: opts.DevProxy == "",
HttpOnly: true,
}},
}
}
@ -75,7 +143,7 @@ type loginForm struct {
Password string
}
func dummyAuth(_ httprouter.Params, body io.Reader) Response {
func dummyAuth(opts *config.Options, _ httprouter.Params, body io.Reader) Response {
var lf loginForm
if err := json.NewDecoder(body).Decode(&lf); err != nil {
return APIErrorResponse{
@ -88,30 +156,11 @@ func dummyAuth(_ httprouter.Params, body io.Reader) Response {
err: err,
}
} else {
session, err := happydns.NewSession(user)
if err != nil {
return APIErrorResponse{
err: err,
}
}
if err := storage.MainStore.CreateSession(session); err != nil {
return APIErrorResponse{
err: err,
}
}
res := map[string]interface{}{}
res["status"] = "OK"
res["id_session"] = session.Id
return APIResponse{
response: res,
}
return completeAuth(opts, user.Email, "dummy")
}
}
func checkAuth(_ httprouter.Params, body io.Reader) Response {
func checkAuth(opts *config.Options, _ httprouter.Params, body io.Reader) Response {
var lf loginForm
if err := json.NewDecoder(body).Decode(&lf); err != nil {
return APIErrorResponse{
@ -129,25 +178,6 @@ func checkAuth(_ httprouter.Params, body io.Reader) Response {
status: http.StatusUnauthorized,
}
} else {
session, err := happydns.NewSession(user)
if err != nil {
return APIErrorResponse{
err: err,
}
}
if err := storage.MainStore.CreateSession(session); err != nil {
return APIErrorResponse{
err: err,
}
}
res := map[string]interface{}{}
res["status"] = "OK"
res["id_session"] = session.Id
return APIResponse{
response: res,
}
return completeAuth(opts, user.Email, "local")
}
}

View File

@ -97,52 +97,66 @@
<script>
import axios from 'axios'
function updateSession (t) {
if (sessionStorage.token !== undefined) {
t.session = sessionStorage.token
axios.defaults.headers.common.Authorization = 'Bearer '.concat(sessionStorage.token)
axios.get('/api/users/auth')
.then(
(response) => {
t.loggedUser = response.data
},
(error) => {
t.$bvToast.toast(
'Invalid session, your have been logged out: ' + error.response.data.errmsg + '. Please login again.', {
title: 'Authentication timeout',
autoHideDelay: 5000,
variant: 'danger',
toaster: 'b-toaster-content-right'
}
)
t.session = null
t.loggedUser = null
delete sessionStorage.token
t.$router.replace('/login')
}
)
}
}
export default {
data: function () {
return {
loggedUser: null,
session: null
loggedUser: null
}
},
mounted () {
updateSession(this)
if (sessionStorage.loggedUser) {
this.loggedUser = JSON.parse(sessionStorage.loggedUser)
}
this.updateSession()
this.$on('login', this.login)
},
methods: {
logout () {
sessionStorage.token = undefined
updateSession(this)
this.$router.push('/')
axios
.post('/api/users/auth/logout')
.then(
(response) => {
delete sessionStorage.loggedUser
this.loggedUser = null
this.updateSession()
this.$router.push('/')
},
(error) => {
this.$bvToast.toast(
'An error occurs when trying to logout: ' + error.response.data.errmsg, {
title: 'Logout error',
autoHideDelay: 5000,
toaster: 'b-toaster-content-right'
}
)
}
)
},
updateSession () {
axios.get('/api/users/auth')
.then(
(response) => {
sessionStorage.loggedUser = JSON.stringify(response.data)
this.loggedUser = response.data
},
(error) => {
delete sessionStorage.loggedUser
this.loggedUser = null
this.$root.$bvToast.toast(
'Invalid session, your have been logged out: ' + error.response.data.errmsg + '. Please login again.', {
title: 'Authentication timeout',
autoHideDelay: 5000,
variant: 'danger',
toaster: 'b-toaster-content-right'
}
)
this.$router.replace('/login')
}
)
},
login (email, password) {
@ -153,13 +167,13 @@ export default {
})
.then(
(response) => {
if (response.data.id_session) {
sessionStorage.token = response.data.id_session
}
updateSession(this)
sessionStorage.loggedUser = JSON.stringify(response.data)
this.loggedUser = response.data
this.$router.push('/')
},
(error) => {
delete sessionStorage.loggedUser
this.loggedUser = null
this.$bvToast.toast(
'An error occurs when trying to login: ' + error.response.data.errmsg, {
title: 'Login error',

View File

@ -41,7 +41,7 @@ import ZoneList from '@/views/domain-list'
export default {
beforeRouteEnter (to, from, next) {
next(vm => {
if (sessionStorage.token === undefined) {
if (sessionStorage.loggedUser === undefined) {
if (to.path === '/') {
var preferedLang = navigator.language.split('-')[0] || process.env.VUE_APP_I18N_LOCALE || 'en'
if (preferedLang !== 'en' && preferedLang !== 'fr') {
@ -61,7 +61,7 @@ export default {
data () {
return {
homeComponent: sessionStorage.token !== undefined ? ZoneList : ''
homeComponent: sessionStorage.loggedUser !== undefined ? ZoneList : ''
}
}
}