ApiFlash captures each screenshot with a fresh browser that isn't logged in. Here is how to give it
access to pages behind a login. Options 1 and 2 require changing the code of the website, so they only
work for websites you own.
<?phpnamespaceApp\Http\Controllers;useApp\Models\Report;useIlluminate\Http\Request;useIlluminate\Support\Facades\Auth;// In routes/web.php: Route::get('/reports/{report}', [ReportController::class, 'show']);// In config/services.php: 'screenshot' => ['secret' => env('SCREENSHOT_SECRET')],classReportControllerextendsController{publicfunctionshow(Request$request,Report$report){if(!Auth::check()&&!$this->isScreenshotRequest($request)){returnredirect()->route('login');}returnview('report',['report'=>$report]);}privatefunctionisScreenshotRequest(Request$request):bool{$secret=config('services.screenshot.secret')?:thrownew\RuntimeException('SCREENSHOT_SECRET is not set.');returnhash_equals($secret,$request->header('X-Screenshot-Secret',''));}}
@ControllerpublicclassReportController{privatestaticfinalbyte[]SCREENSHOT_SECRET=System.getenv("SCREENSHOT_SECRET").getBytes(StandardCharsets.UTF_8);privatefinalReportRepositoryreportRepository;publicReportController(ReportRepositoryreportRepository){this.reportRepository=reportRepository;}staticbooleanisScreenshotRequest(Stringsecret){returnsecret!=null&&MessageDigest.isEqual(secret.getBytes(StandardCharsets.UTF_8),SCREENSHOT_SECRET);}// Also permit "/reports/**" in your Spring Security configuration.@GetMapping("/reports/{id}")publicStringreport(@PathVariablelongid,@RequestHeader(name="X-Screenshot-Secret",required=false)Stringsecret,Principalprincipal,Modelmodel){if(principal==null&&!isScreenshotRequest(secret)){return"redirect:/login";}model.addAttribute("report",reportRepository.findById(id).orElseThrow());return"report";}}
<?phpnamespaceApp\Support;// In config/services.php: 'screenshot' => ['secret' => env('SCREENSHOT_SECRET')],classScreenshotToken{publicstaticfunctiongenerate():string{$expires=(string)(time()+300);return$expires.'.'.self::sign($expires);}publicstaticfunctionisValid(?string$token):bool{$parts=explode('.',$token??'',2);if(count($parts)!==2||!ctype_digit($parts[0])||(int)$parts[0]<time()){returnfalse;}returnhash_equals(self::sign($parts[0]),$parts[1]);}privatestaticfunctionsign(string$value):string{$secret=config('services.screenshot.secret')?:thrownew\RuntimeException('SCREENSHOT_SECRET is not set.');returnhash_hmac('sha256',$value,$secret);}}// In the controller, replace $this->isScreenshotRequest($request) with// ScreenshotToken::isValid($request->header('X-Screenshot-Token')).
1 2 3 4 5 6 7 8 91011121314151617181920
moduleScreenshotTokendefself.generateexpires=(Time.now.to_i+300).to_s"#{expires}.#{sign(expires)}"enddefself.valid?(token)expires,signature=token.to_s.split('.',2)returnfalseunlessexpires&.match?(/\A\d+\z/)&&expires.to_i>Time.now.to_iActiveSupport::SecurityUtils.secure_compare(signature.to_s,sign(expires))enddefself.sign(value)OpenSSL::HMAC.hexdigest('SHA256',ENV.fetch('SCREENSHOT_SECRET'),value)endend# In the controller:# skip_before_action :authenticate_user!, if: -> { ScreenshotToken.valid?(request.headers['X-Screenshot-Token']) }
publicfinalclassScreenshotToken{privatestaticfinalbyte[]SECRET=System.getenv("SCREENSHOT_SECRET").getBytes(StandardCharsets.UTF_8);publicstaticStringgenerate(){Stringexpires=String.valueOf(Instant.now().getEpochSecond()+300);returnexpires+"."+sign(expires);}publicstaticbooleanisValid(Stringtoken){String[]parts=token==null?newString[0]:token.split("\\.",2);if(parts.length!=2||!parts[0].matches("\\d{1,18}")||Long.parseLong(parts[0])<Instant.now().getEpochSecond()){returnfalse;}returnMessageDigest.isEqual(parts[1].getBytes(StandardCharsets.UTF_8),sign(parts[0]).getBytes(StandardCharsets.UTF_8));}privatestaticStringsign(Stringvalue){try{Macmac=Mac.getInstance("HmacSHA256");mac.init(newSecretKeySpec(SECRET,"HmacSHA256"));returnHexFormat.of().formatHex(mac.doFinal(value.getBytes(StandardCharsets.UTF_8)));}catch(GeneralSecurityExceptione){thrownewIllegalStateException(e);}}}// In the controller, replace isScreenshotRequest(secret) with ScreenshotToken.isValid(token).
As the token changes with every API call, screenshots are never served from the cache.
The secret never leaves your server, and a leaked token is useless after 5 minutes.
Option 3 • Pass session cookies
Most websites keep users logged in with a session cookie. Pass that cookie with the cookies
parameter, and ApiFlash loads the page as the logged in user:
To get the session cookie, log in with your browser and copy it from the developer tools
(Application › Cookies). Cookies are only sent to the domain of the url
parameter.
Some session cookies expire. To get fresh ones automatically, log in with a script and build the
cookies parameter from the cookies it receives:
importosimportrequests# Without a browser, this only works with simple login forms that don't need JavaScript or a CSRF token.session=requests.Session()session.post('https://app.example.com/login',data={'email':'screenshots@example.com','password':os.environ['PASSWORD'],})print(';'.join(f'{name}={value}'forname,valueinsession.cookies.items()))
Option 4 • Pass HTTP headers
Some websites and APIs authenticate each request with a header, usually Authorization.
Pass that header with the headers parameter, and ApiFlash sends it when loading the page:
For Basic authentication, use Authorization=Basic CREDENTIALS where
CREDENTIALS is username:password encoded in base64.
Like in option 1, headers are also sent to the third party resources loaded
by the page. Prefer cookies if the website supports them, as they are only
sent to the domain of the url parameter.
Option 5 • Log in with JavaScript
ApiFlash can also log in by itself, by filling in and submitting the login form of the website before
taking the screenshot. Set url to the login page, submit the login form with the js
parameter, and set wait_for to a CSS selector that only exists once logged in:
Where LOGIN_SCRIPT fills in and submits the login form:
1 2 3 4 5 6 7 8 910
functionfill(selector,value){constinput=document.querySelector(selector);// Use the native setter so that frameworks like React notice the change.Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set.call(input,value);input.dispatchEvent(newEvent('input',{bubbles:true}));}fill('input[name="email"]','screenshots@example.com');fill('input[name="password"]','YOUR_PASSWORD');document.querySelector('input[name="password"]').form.requestSubmit();
The screenshot shows the page you land on after the login, which you can often choose with a redirect
parameter like /login?next=/reports/42. This doesn't work with CAPTCHAs or two-factor
authentication.