I am currently developing a web-application using Angular2 with TypeScript (if that matters).
This application has to communicate with a webserver, which is asking for a digest authentication.
Until now i was using the native login prompt of the browsers, which is automatically showing, when the server returns a "401 unauthorized". The browser only asks for authentication once and automatically uses this username and password for future requests. So I don't have to take care about the authentication, the browser does everything for me.
Unfortunately now i have to create a custom login screen, as i have to implement some default actions, such as "register" or "reset passwort", which are ususally accessible from that screen.
As digest authentication is quite complex and the browser would allready do all the complex things for me I would like to continue using the browsers functionality, but without using it's login prompt.
So is it possible to use the browsers authentication functionality?
If it is possible, how can I set the username and the password it should use?
EDIT:
As someone wanted to close this question as "to broad", i'll try to add some more detail.
The web-application gets data from a restful webservice. This webservice requires digest authentication and responds with a 401, if you are using a wrong username or password.
As mentioned above, the browser automatically shows a login prompt, if he gets a 401 error. If you enter a valid login, the browser caches those values somewhere and automatically sets them for every future request.
Now i basicly want to replace the login prompt and programatically set the values the browser should use for the login.
I hope this helps to make the question clear.
Basically you have to write a HTTP decorator to intercept response code 401. Afterwards you add the Authentication header and replay the request.
import { Injectable } from '#angular/core';
import { Http, ConnectionBackend, RequestOptions, RequestOptionsArgs, Response, Headers } from '#angular/http';
import { Observable } from 'rxjs/Rx';
#Injectable()
export class CustomHttp extends Http {
/**
* intercept request
* #param {Observable<Response>} observable to use
* #param {string} url to request
* #returns {Observable<Response>} return value
* #private
*/
private _intercept(observable: Observable<Response>, url: string): Observable<Response> {
return observable.catch(error => {
if (error.status === 401) {
// add custom header
let headers = new Headers();
headers.append('Authentication', '<HEADER>');
// replay request with modified header
return this.get(url, new RequestOptions({
headers: headers
});
} else {
return Observable.throw(error);
}
});
};
/**
* constructor
* #param {ConnectionBackend} backend to use
* #param {RequestOptions} defaultOptions to use
* #returns {void} nothing
*/
constructor(backend: ConnectionBackend, defaultOptions: RequestOptions) {
super(backend, defaultOptions);
};
/**
* get request
* #param {string} url to request
* #param {RequestOptionsArgs} options to use
* #returns {Observable<Response>} return value
*/
get(url: string, options?: RequestOptionsArgs): Observable<Response> {
return this._intercept(super.get(url, options), url);
};
}
What didn’t worked for me yet is determining correct responses as there was a bug filed against Angular2 (https://github.com/angular/angular/pull/9355), which was merged just a few days before.
You have to increment the request counter for each valid request following the first successful one.
Maybe somebody else can show up with a working solution.
Related
I want to access the res object to send httpOnly cookies and need to validate body with DTO.
but every time i try to do it something go wrong what is the right order for these params?
There are no order.
Also, they are parameter decorator factories, not parameters.
There is no strict order that needs to be followed. Each controller method may use decorators for retrieving different things (see controller docs: https://docs.nestjs.com/controllers)
Example
Let's imagine you are building an endpoint for handling some kind of search using a POST request and a payload. Nest returns some results and sets cookies with the latest performed search timestamp.
That sounds like your requirements, right?
Add cookie parser to nest application
Make sure you followed cookies documentation and installed all dependencies together and configured cookie parser middleware: https://docs.nestjs.com/techniques/cookies
Search payload data transfer object (DTO)
import { IsInt, IsNotEmpty } from 'class-validator';
export class SearchBodyDto {
#IsNotEmpty()
searchPhrase: string;
#IsInt()
page = 1;
}
Controller method
import { Request, Response } from 'express';
import { Body, Controller, Post, Req, Res } from '#nestjs/common';
#Controller()
export class AppController {
#Post('/search')
async search(
#Body() body: SearchBodyDto,
#Req() req: Request,
// passthrough:true here leaves response handling to framework.
// Otherwise you would need to send response manually, like: `res.json({data})`
#Res({ passthrough: true }) res: Response,
) {
const currentTimestamp = new Date().toISOString();
// Save to cookies current timestamp of search.
res.cookie('lastSearch', currentTimestamp, { httpOnly: true });
return {
// Return last search timestamp and fallback to current timestamp.
lastSearch: req.cookies.lastSearch ?? currentTimestamp,
// Validated search phrase from DTO.
searchPhrase: body.searchPhrase,
// Page by default 1.
page: body.page,
// Some example search results.
searchResults: ['water', 'wind', 'earth'],
};
}
}
Result
Now when you do a request to the endpoint, you will see the latest search time in a response: postman example, and also that value will be set to 'lastSearch' cookie.
The payload still will be validated using decorators on DTO.
I recently enabled Angular Universal in my project. Everything works as expected, except for one strange issue. Whenever I refresh page or click a link in email that navigates to webpage, I see Login page for brief second then actual page loads.
Sample project created and uploaded to Github. Remember the delay may not as long as in my real project.
Github repo link: https://github.com/pavankjadda/angular-ssr-docker
Problem:
As it turns out when Ng Express Engine loads the web page, it does not have access to cookies. Hence it redirects user to Login page, but as soon as browser loads JavaScript (Angular), which checks for cookies and validates Authentication guards, redirects user to actual webpage. The ideal solution would be making cookies available on server side (sending it through request) and making sure Authentication guards passes. I tried send the cookies through server.ts, but couldn't get it working.
Work Around:
Until I figure out the solution here is the work around I followed. Whenever we check for cookies, determine if the platform is server, if yes return true. Here are the few places where you can make this change
Make sure authservice.ts returns true when the platform is server
/**
* Returns true if the 'isLoggedIn' cookie is 'true', otherwise returns false
*
* #author Pavan Kumar Jadda
* #since 1.0.0
*/
isUserLoggedIn(): boolean {
return isPlatformServer(this.platformId) || (this.cookieService.get('isLoggedIn') === 'true' && this.cookieService.check('X-Auth-Token'));
}
Do the same thing in Authentication guards
#Injectable({
providedIn: 'root'
})
export class CoreUserAuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router, #Inject(PLATFORM_ID) private platformId: any,) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
const url: string = state.url;
return this.checkLogin(url);
}
/**
* Returns true if the user is Logged In and has Core user role
*
* #author Pavan Kumar Jadda
* #since 1.0.0
*/
private checkLogin(url: string): boolean {
// Return if the platform is server
if (isPlatformServer(this.platformId))
return true;
if (this.authService.isUserLoggedIn() && this.authService.hasCoreUserRole()) {
return true;
}
if (this.authService.isUserLoggedIn() && !this.authService.hasCoreUserRole()) {
this.router.navigate(['/unauthorized']);
}
// Store the attempted URL for redirecting
this.authService.redirectUrl = url;
// Navigate to the login page with extras
this.router.navigate(['/login']);
return false;
}
}
Note: Added work around here, in case if anyone has similar problem. When I have an actual solution to the problem, I will update this answer.
change your isUserLoggedIn() function in auth service to:
public async isUserLoggedIn(): Promise<boolean> {
const isLoggedIn = await this.cookieService.check("token");
return isLoggedIn;
}
In the code below, 'Test' button triggers a function which calls an external endpoint to load data. However, nothing happens when the button is clicked and I get a 400 error in the console area saying Invalid Argument.
Code.gs
function buildAddOn(e) {
// Create a section for that contains all user Labels.
var section = CardService.newCardSection()
var action = CardService.newAction()
.setFunctionName("testCall");
var button = CardService.newTextButton().setText('Test').setOnClickAction(action);
section.addWidget(CardService.newButtonSet().addButton(button));
// section.addWidget(CardService.newTextParagraph()
// .setText("This is a text paragraph widget. Multiple lines are allowed if needed.");)
// Build the main card after adding the section.
var card = CardService.newCardBuilder()
.setHeader(CardService.newCardHeader()
.setTitle('Authentication Card')
.setImageUrl('https://www.gstatic.com/images/icons/material/system/1x/label_googblue_48dp.png'))
.addSection(section)
.build();
return [card];
}
function testCall(){
console.log("test");
var data = accessProtectedResource('https://api.ssdf.io/v1.0/asd/4/174203','get');
return CardService.newActionResponseBuilder()
.setNotification(CardService.newNotification()
.setType(CardService.NotificationType.INFO)
.setText(data))
.build();
}
authService.gs
/**
* Attempts to access a non-Google API using a constructed service
* object.
*
* If your add-on needs access to non-Google APIs that require OAuth,
* you need to implement this method. You can use the OAuth1 and
* OAuth2 Apps Script libraries to help implement it.
*
* #param {String} url The URL to access.
* #param {String} method_opt The HTTP method. Defaults to GET.
* #param {Object} headers_opt The HTTP headers. Defaults to an empty
* object. The Authorization field is added
* to the headers in this method.
* #return {HttpResponse} the result from the UrlFetchApp.fetch() call.
*/
function accessProtectedResource(url, method_opt, headers_opt) {
var service = getOAuthService();
var maybeAuthorized = service.hasAccess();
if (maybeAuthorized) {
// A token is present, but it may be expired or invalid. Make a
// request and check the response code to be sure.
// Make the UrlFetch request and return the result.
var accessToken = service.getAccessToken();
var method = method_opt || 'get';
var headers = headers_opt || {};
headers['Authorization'] =
Utilities.formatString('Bearer %s', accessToken);
var resp = UrlFetchApp.fetch(url, {
'headers': headers,
'method' : method,
'muteHttpExceptions': true, // Prevents thrown HTTP exceptions.
});
var code = resp.getResponseCode();
if (code >= 200 && code < 300) {
return resp.getContentText("utf-8"); // Success
} else if (code == 401 || code == 403) {
// Not fully authorized for this action.
maybeAuthorized = false;
} else {
// Handle other response codes by logging them and throwing an
// exception.
console.error("Backend server error (%s): %s", code.toString(),
resp.getContentText("utf-8"));
throw ("Backend server error: " + code);
}
}
if (!maybeAuthorized) {
// Invoke the authorization flow using the default authorization
// prompt card.
CardService.newAuthorizationException()
.setAuthorizationUrl(service.getAuthorizationUrl())
.setResourceDisplayName("Login to ....")
.throwException();
}
}
/**
* Create a new OAuth service to facilitate accessing an API.
* This example assumes there is a single service that the add-on needs to
* access. Its name is used when persisting the authorized token, so ensure
* it is unique within the scope of the property store. You must set the
* client secret and client ID, which are obtained when registering your
* add-on with the API.
*
* See the Apps Script OAuth2 Library documentation for more
* information:
* https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
*
* #return A configured OAuth2 service object.
*/
function getOAuthService() {
return OAuth2.createService('auth')
.setAuthorizationBaseUrl('https://app.ss.io/oauth/authorize')
.setTokenUrl('https://api.ss.io/oauth/token')
.setClientId('2361c9fbc5ba4b88813a3ef')
.setClientSecret('f5d3a04f4asda30a52830e230e43727')
.setScope('1')
.setCallbackFunction('authCallback')
.setCache(CacheService.getUserCache())
.setPropertyStore(PropertiesService.getUserProperties());
}
/**
* Boilerplate code to determine if a request is authorized and returns
* a corresponding HTML message. When the user completes the OAuth2 flow
* on the service provider's website, this function is invoked from the
* service. In order for authorization to succeed you must make sure that
* the service knows how to call this function by setting the correct
* redirect URL.
*
* The redirect URL to enter is:
* https://script.google.com/macros/d/<Apps Script ID>/usercallback
*
* See the Apps Script OAuth2 Library documentation for more
* information:
* https://github.com/googlesamples/apps-script-oauth2#1-create-the-oauth2-service
*
* #param {Object} callbackRequest The request data received from the
* callback function. Pass it to the service's
* handleCallback() method to complete the
* authorization process.
* #return {HtmlOutput} a success or denied HTML message to display to
* the user. Also sets a timer to close the window
* automatically.
*/
function authCallback(callbackRequest) {
var authorized = getOAuthService().handleCallback(callbackRequest);
if (authorized) {
return HtmlService.createHtmlOutput(
'Success! <script>setTimeout(function() { top.window.close() }, 1);</script>');
} else {
return HtmlService.createHtmlOutput('Denied');
}
}
/**
* Unauthorizes the non-Google service. This is useful for OAuth
* development/testing. Run this method (Run > resetOAuth in the script
* editor) to reset OAuth to re-prompt the user for OAuth.
*/
function resetOAuth() {
getOAuthService().reset();
}
All URLs in the function getOAuthService() have to be the original google-URLs of the example:
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
and
// Set the scopes to request (space-separated for Google services).
.setScope('https://www.googleapis.com/auth/drive')
It might be possible to change the latter one, but only the path, not the domain, and you've to look in the API if and how it is adjustable. Also this scope-parameter differs in your example, but I don't know if '1' could be accepted.
Your own application get's a feedback if access is granted but is not involved in validating the authentication. Therefore you also need to get the access-token, I see it in your code: var accessToken = service.getAccessToken();, in the example it looks a bit different:
function makeRequest() {
var driveService = getDriveService();
var response = UrlFetchApp.fetch('https://www.googleapis.com/drive/v2/files?maxResults=10', {
headers: {
Authorization: 'Bearer ' + driveService.getAccessToken()
}
});
// ...
}
see at the line with Authorization: 'Bearer ' + driveService.getAccessToken().
Your own server isn't (and shouldn't be) configured to handle authentication requests and therefore throws the 400 error. The API is built for use by Javascript on client-side, therefore I advise not to use it to authenticate on an own server. Nevertheless below I listed APIs for usage on your own server.
Using your own server for authentication
If you dismiss my advise to use the google-servers for authentication, then the scope of the tagged issues is getting larger, as server-configuration (apache, nginx, ...) and server-side programming-languages (PHP, Python, ...) might be involved.
You've to debug the headers then, what is sent exactly to the server and why the server can't handle it. You can debug the request and response in the browser's developer-tool (network panel) and check the error-files of the server.
The server has to acquire the google-service then by itself and do what would be done in frontend only by javascript if you'd follow the example you linked in your code.
In the example are three server-request made:
- one to authenticate
- one to get the authentication-token
- one to get the protected service
You've to keep in mind that all three steps have to be done then by your own server and that your server has to be able to answer on all three request-types.
An important question might be which of the three requests is producing the error?
You should extend your question then with the detailed problems you discover concerning communication with your server.
APIs and Examples for server-side OAuth-athentication
PHP: https://developers.google.com/api-client-library/php/auth/web-app
Python: https://developers.google.com/api-client-library/python/auth/web-app
Ruby: https://developers.google.com/api-client-library/ruby/auth/web-app
NodeJs: https://developers.google.com/apps-script/api/quickstart/nodejs
Java: https://developers.google.com/api-client-library/java/google-oauth-java-client/
Go: https://github.com/googleapis/google-api-go-client
.NET: https://developers.google.com/api-client-library/dotnet/apis/oauth2/v2
The 400 error code seems to refer to a bad link, I think you need to check your different URLs.
And particularly in the getOAuthService, you have written: "https://apP.ss.io/oauth/authorize" in the authorization base URL and "https://apI.ss.io/oauth/token" in the token URL.
In Aurelia, there doesn't seem to be any support for CSRF protection yet, as opposed to AngularJS's XSRF-TOKEN header which is set automatically on all XHR requests by the AngularJS framework.
How should I go about protecting an Aurelia app from CSRF attacks? Should I roll my own support based on the OWASP CSRF Prevention Cheat Sheet, or are there any alternatives out there for Aurelia already?
You should be able to do this yourself fairly easily by using Aurelia's HTTP interceptors (see examples in the docs). Before every request, you can send your token. This can be done with both the conventional aurelia-http-client and the new standard aurelia-fetch-client.
Your code might look like this:
export class MyRestAPI {
static inject () { return [HttpClient]; } // This could easily be fetch-client
constructor (http) {
this.http = http.configure(x => {
x.withBaseUrl(myBaseUrl);
x.useStandardConfiguration();
x.withInterceptor({
request: function (request) {
request.headers.set('XSRF-TOKEN', myAwesomeToken);
return request;
}
});
});
}
...
}
On every request, your token would be sent. You'd have to handle the validation on the server side. You could easily set up your code so that your initial request could grab a token, or you could pass a token back as part of your authentication payload, or if you wanted to you could even store a token in the browser's localstorage and use it that way.
You could even go a step further and implement JWT authentication. If you're using node.js, I have a small blog post that describes how I implemented JWT in Express. There's a plugin on Github called aurelia-auth that handles JWT, and there's a blog post on its implementation on the Aurelia blog as well.
Here is a sample interceptor that reads the token from the response header if it exists and sets it automatically on every request that needs it.
import {Interceptor, HttpResponseMessage, RequestMessage} from "aurelia-http-client";
class CsrfHeaderInterceptor implements Interceptor {
private static readonly TOKEN_HEADER = 'X-CSRF-Token';
private latestCsrfToken: string;
response(response: HttpResponseMessage): HttpResponseMessage {
if (response.headers.has(CsrfHeaderInterceptor.TOKEN_HEADER)) {
this.latestCsrfToken = response.headers.get(CsrfHeaderInterceptor.TOKEN_HEADER);
}
return response;
}
request(request: RequestMessage): RequestMessage {
if (this.latestCsrfToken) {
if (['POST', 'PUT', 'PATCH'].indexOf(request.method) >= 0) {
request.headers.add(CsrfHeaderInterceptor.TOKEN_HEADER, this.latestCsrfToken);
}
}
return request;
}
}
You register it in your http/fetch client with for example:
httpClient.configure((config) => {
config
.withBaseUrl("/api/") // adjust to your needs
.withHeader('Accept', 'application/json') // adjust to your needs
.withHeader('X-Requested-With', 'XMLHttpRequest') // adjust to your needs
.withInterceptor(new CsrfHeaderInterceptor());
});
If I log in to my web app, wait for the session to expire, then make an ajax request with a form in my web app I get the following error show up in the console:
Failed to load resource: the server responded with a status of 500 (Internal Server Error)
Ideally what would happen is a redirect to the login page or an error message shows up under the form that fired the ajax request (i.e. something meaningful to the user). It may be worth noting I already have the client side code to throw up errors to show an error message to the user if they make a validation error on the form.
I believe I have an idea how to check if the session is expired and return something useful to the user telling them to login but I'm unsure how I'd implement this globally. So I'm wondering is it possible to handle this issue globally from the back end in Laravel and (or) do I need to write some logic for each ajax request to catch the issue to show an error message client side?
I'm using Laravel and Javascript/JQuery. Thanks for any help!
Here's the quick solution for your case:
Controller (e.g. AuthController.php):
/**
* Check user session.
*
* #return Response
*/
public function checkSession()
{
return Response::json(['guest' => Auth::guest()]);
}
Also, probably it will be needed to add this method name to ignore by guest middleware:
$this->middleware('guest', ['except' => ['logout', 'checkSession']]);
Route:
Route::get('check-session', 'Auth\AuthController#checkSession');
Layout (JS part), only for signed in users:
#if (Auth::user())
<script>
$(function() {
setInterval(function checkSession() {
$.get('/check-session', function(data) {
// if session was expired
if (data.guest) {
// redirect to login page
// location.assign('/auth/login');
// or, may be better, just reload page
location.reload();
}
});
}, 60000); // every minute
});
</script>
#endif
use Middleware
Middleware:
<?php namespace App\Http\Middleware;
class OldMiddleware {
/**
* Run the request filter.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(!**condition to check login**)
{
// if session is expired
return response()->json(['message' => 'Forbidden!'],403);
}
return $next($request);
}
}
Route:
Route::group(['middleware' => '\App\Http\Middleware\OldMiddleware'], function(){
//put the routes which needs authentication to complete
});
View:
$.ajax({
type: 'post',
url: {{route('someroute')}}
//etc etc
}).done(function(data){
//do if request succeeds
}).fail(function(x,y,z){
//show error message or redirect since it is failed.
});