mirror of https://github.com/portainer/portainer
feat(auth): save jwt in cookie [EE-5864] (#10527)
parent
ecce501cf3
commit
436da01bce
@ -0,0 +1,62 @@
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
gorillacsrf "github.com/gorilla/csrf"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
func WithProtect(handler http.Handler) (http.Handler, error) {
|
||||
handler = withSendCSRFToken(handler)
|
||||
|
||||
token := make([]byte, 32)
|
||||
_, err := rand.Read(token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate CSRF token: %w", err)
|
||||
}
|
||||
|
||||
handler = gorillacsrf.Protect([]byte(token), gorillacsrf.Path("/"))(handler)
|
||||
|
||||
return withSkipCSRF(handler), nil
|
||||
}
|
||||
|
||||
func withSendCSRFToken(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
sw := negroni.NewResponseWriter(w)
|
||||
|
||||
sw.Before(func(sw negroni.ResponseWriter) {
|
||||
statusCode := sw.Status()
|
||||
if statusCode >= 200 && statusCode < 300 {
|
||||
csrfToken := gorillacsrf.Token(r)
|
||||
sw.Header().Set("X-CSRF-Token", csrfToken)
|
||||
}
|
||||
})
|
||||
|
||||
handler.ServeHTTP(sw, r)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func withSkipCSRF(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
skip, err := security.ShouldSkipCSRFCheck(r)
|
||||
if err != nil {
|
||||
httperror.WriteError(w, http.StatusForbidden, err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
if skip {
|
||||
r = gorillacsrf.UnsafeSkipCheck(r)
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
type CurrentUserInspectResponse struct {
|
||||
*portainer.User
|
||||
ForceChangePassword bool `json:"forceChangePassword"`
|
||||
}
|
||||
|
||||
// @id CurrentUserInspect
|
||||
// @summary Inspect the current user user
|
||||
// @description Retrieve details about the current user.
|
||||
// @description User passwords are filtered out, and should never be accessible.
|
||||
// @description **Access policy**: authenticated
|
||||
// @tags users
|
||||
// @security ApiKeyAuth
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 {object} portainer.User "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied"
|
||||
// @failure 404 "User not found"
|
||||
// @failure 500 "Server error"
|
||||
// @router /users/me [get]
|
||||
func (handler *Handler) userInspectMe(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve info from request context", err)
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().Read(securityContext.UserID)
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a user with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find a user with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
forceChangePassword := !handler.passwordStrengthChecker.Check(user.Password)
|
||||
|
||||
hideFields(user)
|
||||
return response.JSON(w, &CurrentUserInspectResponse{User: user, ForceChangePassword: forceChangePassword})
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import { AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import { IHttpResponse } from 'angular';
|
||||
|
||||
import axios from './axios';
|
||||
|
||||
axios.interceptors.response.use(csrfTokenReaderInterceptor);
|
||||
axios.interceptors.request.use(csrfInterceptor);
|
||||
|
||||
let csrfToken: string | null = null;
|
||||
|
||||
export function csrfTokenReaderInterceptor(config: AxiosResponse) {
|
||||
const csrfTokenHeader = config.headers['x-csrf-token'];
|
||||
if (csrfTokenHeader) {
|
||||
csrfToken = csrfTokenHeader;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
export function csrfTokenReaderInterceptorAngular(
|
||||
config: IHttpResponse<unknown>
|
||||
) {
|
||||
const csrfTokenHeader = config.headers('x-csrf-token');
|
||||
if (csrfTokenHeader) {
|
||||
csrfToken = csrfTokenHeader;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
export function csrfInterceptor(config: AxiosRequestConfig) {
|
||||
if (!csrfToken) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const newConfig = { headers: config.headers || {}, ...config };
|
||||
newConfig.headers['X-CSRF-Token'] = csrfToken;
|
||||
return newConfig;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
|
||||
import { buildUrl } from '../user.service';
|
||||
import { User } from '../types';
|
||||
|
||||
import { queryKeys } from './queryKeys';
|
||||
|
||||
interface CurrentUserResponse extends User {
|
||||
forceChangePassword: boolean;
|
||||
}
|
||||
|
||||
export function useLoadCurrentUser({ staleTime }: { staleTime?: number } = {}) {
|
||||
return useQuery(queryKeys.me(), () => getCurrentUser(), {
|
||||
...withError('Unable to retrieve user details'),
|
||||
staleTime,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getCurrentUser() {
|
||||
try {
|
||||
const { data: user } = await axios.get<CurrentUserResponse>(
|
||||
buildUrl(undefined, 'me')
|
||||
);
|
||||
|
||||
return user;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve user details');
|
||||
}
|
||||
}
|
Loading…
Reference in new issue