Merge pull request #12826 from goharbor/csrf-local

CSRF token cookie -> header
This commit is contained in:
Will Sun 2020-08-20 17:42:49 +08:00 committed by GitHub
commit 29f3ced3ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 60 additions and 43 deletions

View File

@ -1,20 +1,10 @@
import { TestBed, inject } from '@angular/core/testing'; import { TestBed, inject } from '@angular/core/testing';
import { InterceptHttpService } from './intercept-http.service'; import { InterceptHttpService } from './intercept-http.service';
import { CookieService } from 'ngx-cookie';
import { HttpRequest, HttpResponse } from '@angular/common/http'; import { HttpRequest, HttpResponse } from '@angular/common/http';
import { of, throwError } from 'rxjs'; import { of, throwError } from 'rxjs';
describe('InterceptHttpService', () => { describe('InterceptHttpService', () => {
let cookie = "fdsa|ds"; const mockedCSRFToken: string = 'test';
const mockCookieService = {
get: function () {
return cookie;
},
set: function (cookieStr: string) {
cookie = cookieStr;
}
};
const mockRequest = new HttpRequest('PUT', "", { const mockRequest = new HttpRequest('PUT', "", {
headers: new Map() headers: new Map()
}); });
@ -29,13 +19,21 @@ describe('InterceptHttpService', () => {
} }
} }
}; };
beforeEach(() => TestBed.configureTestingModule({}));
beforeEach(() => { beforeEach(() => {
let store = {};
spyOn(localStorage, 'getItem').and.callFake( key => {
return store[key];
});
spyOn(localStorage, 'setItem').and.callFake((key, value) => {
return store[key] = value + '';
});
spyOn(localStorage, 'clear').and.callFake( () => {
store = {};
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [],
providers: [ providers: [
InterceptHttpService, InterceptHttpService
{ provide: CookieService, useValue: mockCookieService }
] ]
}); });
@ -46,10 +44,10 @@ describe('InterceptHttpService', () => {
it('should be get right token and send right request when the cookie not exists', inject([InterceptHttpService], it('should be get right token and send right request when the cookie not exists', inject([InterceptHttpService],
(service: InterceptHttpService) => { (service: InterceptHttpService) => {
mockCookieService.set("fdsa|ds"); localStorage.setItem("__csrf", mockedCSRFToken);
service.intercept(mockRequest, mockHandle).subscribe(res => { service.intercept(mockRequest, mockHandle).subscribe(res => {
if (res.status === 403) { if (res.status === 403) {
expect(mockRequest.headers.get("X-Harbor-CSRF-Token")).toEqual(cookie); expect(mockRequest.headers.get("X-Harbor-CSRF-Token")).toEqual(mockedCSRFToken);
} else { } else {
expect(res.status).toEqual(200); expect(res.status).toEqual(200);
} }

View File

@ -1,28 +1,58 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs'; import { Observable, throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
import { CookieService } from 'ngx-cookie';
const SAFE_METHODS: string[] = ["GET", "HEAD", "OPTIONS", "TRACE"];
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class InterceptHttpService implements HttpInterceptor { export class InterceptHttpService implements HttpInterceptor {
constructor(private cookie: CookieService) { } constructor() { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> { intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
// Get the csrf token from localstorage
return next.handle(request).pipe(catchError(error => { const token = localStorage.getItem("__csrf");
if (error.status === 403) { if (token) {
let Xsrftoken = this.cookie.get("__csrf"); // Clone the request and replace the original headers with
if (Xsrftoken && !request.headers.has('X-Harbor-CSRF-Token')) { // cloned headers, updated with the csrf token.
request = request.clone({ headers: request.headers.set('X-Harbor-CSRF-Token', Xsrftoken) }); // not for requests using safe methods
return next.handle(request); if (request.method && SAFE_METHODS.indexOf(request.method.toUpperCase()) === -1) {
} request = request.clone({
headers: request.headers.set('X-Harbor-CSRF-Token', token)
});
} }
return throwError(error); }
})); return next.handle(request).pipe(
tap(response => {
if (response && response instanceof HttpResponse && response.headers) {
const responseToken: string = response.headers.get('X-Harbor-CSRF-Token');
if (responseToken) {
localStorage.setItem("__csrf", responseToken);
}
}
},
error => {
if (error && error.headers) {
const responseToken: string = error.headers.get('X-Harbor-CSRF-Token');
if (responseToken) {
localStorage.setItem("__csrf", responseToken);
}
}
}))
.pipe(
catchError(error => {
if (error.status === 403) {
const csrfToken = localStorage.getItem("__csrf");
if (csrfToken) {
request = request.clone({ headers: request.headers.set('X-Harbor-CSRF-Token', csrfToken)});
return next.handle(request);
}
}
return throwError(error);
}));
} }
} }

View File

@ -36,10 +36,6 @@ export function GeneralTranslatorLoader(http: HttpClient, config: IServiceConfig
imports: [ imports: [
CommonModule, CommonModule,
HttpClientModule, HttpClientModule,
HttpClientXsrfModule.withOptions({
cookieName: '__csrf',
headerName: 'X-Harbor-CSRF-Token'
}),
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
ClipboardModule, ClipboardModule,

View File

@ -19,7 +19,6 @@ import (
const ( const (
csrfKeyEnv = "CSRF_KEY" csrfKeyEnv = "CSRF_KEY"
tokenHeader = "X-Harbor-CSRF-Token" tokenHeader = "X-Harbor-CSRF-Token"
tokenCookie = "__csrf"
) )
var ( var (
@ -31,13 +30,7 @@ var (
// attachToken makes sure if csrf generate a new token it will be included in the response header // attachToken makes sure if csrf generate a new token it will be included in the response header
func attachToken(w http.ResponseWriter, r *http.Request) { func attachToken(w http.ResponseWriter, r *http.Request) {
if t := csrf.Token(r); len(t) > 0 { if t := csrf.Token(r); len(t) > 0 {
http.SetCookie(w, &http.Cookie{ w.Header().Set(tokenHeader, t)
Name: tokenCookie,
Secure: secureFlag,
Value: t,
Path: "/",
SameSite: http.SameSiteStrictMode,
})
} else { } else {
log.Warningf("token not found in context, skip attaching") log.Warningf("token not found in context, skip attaching")
} }

View File

@ -58,7 +58,7 @@ func TestMiddleware(t *testing.T) {
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
srv.ServeHTTP(rec, c.req) srv.ServeHTTP(rec, c.req)
assert.Equal(t, c.statusCode, rec.Result().StatusCode) assert.Equal(t, c.statusCode, rec.Result().StatusCode)
assert.Equal(t, c.returnToken, hasCookie(rec.Result(), tokenCookie)) assert.Equal(t, c.returnToken, rec.Result().Header.Get(tokenHeader) != "")
} }
} }