Akka authenticateOAuth2Async: credentials missing - authentication

I have the following:
def myUserPassAuthenticator(credentials: Credentials): Future[Option[String]] = {
log.info(credentials.toString)
credentials match {
case p#Credentials.Provided(id) if p.verify("a") =>
log.info("Login success!")
Future.successful(Some(id))
case _ =>
log.info("Login failure!")
Future.successful(None)
}
}
val authRoute = path("login") {
authenticateOAuth2Async(realm = "secure site", myUserPassAuthenticator) { userName =>
complete(s"The user is '$userName'")
}
}
when navigating to that endpoint and entering credentials, the log line
log.info(credentials.toString)
just becomes Missing. What is wrong here?
The content-type of the request is "application/x-www-form-urlencoded"
and the data is "grant_type=password&username=INSERT_USERNAME_HERE&password=INSERT_PWD_HERE"

You should wrap your route in Route.seal directive:
val authRoute =
Route.seal {
path("login") {
authenticateOAuth2Async(realm = "secure site", myUserPassAuthenticator) { userName =>
complete(s"The user is '$userName'")
}
}
}
from documentation it seems to rely on default rejection 401 if authentication fails
Given a function returning Some[T] upon successful authentication and
None otherwise, respectively applies the inner route or rejects the
request with a AuthenticationFailedRejection rejection, which by
default is mapped to an 401 Unauthorized response.
and the Route.seal provides exactly that:
A Route can be "sealed" using Route.seal, which relies on the in-scope
RejectionHandler and ExceptionHandler instances to convert rejections
and exceptions into appropriate HTTP responses for the client.

Related

Google Identity Services : How to refresh access_token for Google API after one hour?

I have implemented the new Google Identity Services to get an access_token to call the Youtube API.
I try to use this on an Angular app.
this.tokenClient = google.accounts.oauth2.initTokenClient({
client_id: googleApiClientId,
scope: 'https://www.googleapis.com/auth/youtube.readonly',
callback: (tokenResponse) => {
this.accessToken = tokenResponse.access_token;
},
});
When I call this.tokenClient.requestAccessToken(), I can get an access token and use the Youtube API, that works.
But after one hour, this token expires. I have this error : "Request had invalid authentication credentials."
How can I get the newly refreshed access_token transparently for the user ?
There are two authorization flows for the Google Identity Services (GIS) library:
The implicit flow, which is client-side only and uses .requestAccessToken()
The authorization code flow, which requires a backend (server-side) as well and uses .requestCode()
With the implicit flow (which is what you are using), there are no refresh tokens. It is up to the client to detect tokens aging out and to re-run the token request flow. Here is some sample code from google's examples for how to handle this:
// initialize the client
tokenClient = google.accounts.oauth2.initTokenClient({
client_id: 'YOUR_CLIENT_ID',
scope: 'https://www.googleapis.com/auth/calendar.readonly',
prompt: 'consent',
callback: '', // defined at request time in await/promise scope.
});
// handler for when token expires
async function getToken(err) {
if (err.result.error.code == 401 || (err.result.error.code == 403) &&
(err.result.error.status == "PERMISSION_DENIED")) {
// The access token is missing, invalid, or expired, prompt for user consent to obtain one.
await new Promise((resolve, reject) => {
try {
// Settle this promise in the response callback for requestAccessToken()
tokenClient.callback = (resp) => {
if (resp.error !== undefined) {
reject(resp);
}
// GIS has automatically updated gapi.client with the newly issued access token.
console.log('gapi.client access token: ' + JSON.stringify(gapi.client.getToken()));
resolve(resp);
};
tokenClient.requestAccessToken();
} catch (err) {
console.log(err)
}
});
} else {
// Errors unrelated to authorization: server errors, exceeding quota, bad requests, and so on.
throw new Error(err);
}
}
// make the request
function showEvents() {
// Try to fetch a list of Calendar events. If a valid access token is needed,
// prompt to obtain one and then retry the original request.
gapi.client.calendar.events.list({ 'calendarId': 'primary' })
.then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
.catch(err => getToken(err)) // for authorization errors obtain an access token
.then(retry => gapi.client.calendar.events.list({ 'calendarId': 'primary' }))
.then(calendarAPIResponse => console.log(JSON.stringify(calendarAPIResponse)))
.catch(err => console.log(err)); // cancelled by user, timeout, etc.
}
Unfortunately GIS doesn't handle any of the token refreshing for you the way that GAPI did, so you will probably want to wrap your access in some common retry logic.
The important bits are that the status code will be a 401 or 403 and the status will be PERMISSION_DENIED.
You can see the details of this example here, toggle to the async/await tab to see the full code.
To refresh the access token in a transparent way for the end-user you have to use the Refresh Token, This token will also come in the response to your call.
With this token, you can do a POST call to the URL: https://www.googleapis.com/oauth2/v4/token with the following request body
client_id: <YOUR_CLIENT_ID>
client_secret: <YOUR_CLIENT_SECRET>
refresh_token: <REFRESH_TOKEN_FOR_THE_USER>
grant_type: refresh_token
refresh token never expires so you can use it any number of times. The response will be a JSON like this:
{
"access_token": "your refreshed access token",
"expires_in": 3599,
"scope": "Set of scope which you have given",
"token_type": "Bearer"
}
#victor-navarro's answer is correct, but I think the URL is wrong.
I made a POST call to https://oauth2.googleapis.com/token with a body like this and it worked for me:
client_id: <YOUR_CLIENT_ID>
client_secret: <YOUR_CLIENT_SECRET>
refresh_token: <REFRESH_TOKEN_FOR_THE_USER>
grant_type: refresh_token

PassportJS OAuth2Strategy: authenticate returns 400 instead of redirecting

I'm trying to setup discord oauth2 pkce using passportjs and the passport-oauth2
const discordStrategy = new OAuth2Strategy({
authorizationURL: 'https://discord.com/api/oauth2/authorize',
tokenURL: 'https://discord.com/api/oauth2/token',
clientID: DISCORD_CLIENT_ID,
clientSecret: DISCORD_CLIENT_SECRET,
callbackURL: DISCORD_CALLBACK_URL,
state: true,
pkce: true,
scope: ['identity', 'scope'],
passReqToCallback: true,
},
(req: Request, accessToken: string, refreshToken: string, profile: DiscordUserProfile, cb: any) => {
prisma.user.findUnique({ where: { email: profile.email ?? '' }}).then(foundUser => {
if (foundUser === null) {
// Create a new user with oauth identity.
} else {
cb(null, foundUser)
}
}).catch(error => {
cb(error, null);
})
});
I've been following the google example as well as some others, these examples indicate that, I should be able to use:
passport.use('discord', discordStrategy);
and
authRouter.get('/discord', passport.authenticate('discord'));
and this should redirect to the OAuth2 providers login page, but instead, I get a 400 Bad Request "The request cannot be fulfilled due to bad syntax." The response body contains an object:
{"scope": ["0"]}
Why is this happening instead of the expected redirect?
My intention is that, once the user logs in, I should get a code, then I can post that code and the code verifier to get an access token, then once the access token is obtained, the actual authenticate call can be made
Edit: I put breakpoints in the passport.authenticate function and I stepped through it. It does actually get through everything and it calls the redirect. The parsed URL it generates, even if I copy it and manually navigate to the URL, it gives me the same, just gives:
{"scope": ["0"]}
and no login page, why?
If you add a version number to the base api url, e.g. /v9 it gives a full error message.
It turned out I had typo'd the scopes, I had 'identity' instead of 'identify' - now this part of the process is working as expected.

Can you customize Ktor 401 - Unauthorized Response?

When implementing Basic Authentication on Ktor and configuring a Provider, which validates whether the credentials are legit by returning a non null Principal, like in this example:
install(Authentication) {
basic("auth-basic") {
realm = "Access to the '/' path"
validate { credentials ->
if (credentials.name == "fernando" && credentials.password == "foobar") {
UserIdPrincipal(credentials.name)
} else {
null
}
}
}
}
If the credentials are invalid and a null is returned, then Ktor automatically communicates with the client by triggering a 401 - Unauthorized, which in terms of behavior is what is expected...
But I cannot provide/add any extra information, like for example where exactly the issue was: username or password.
Any idea on how to mitigate this?
for resolve this problem you can use StatusPages by install it on application calss.
like below:
install(StatusPages) {
status(HttpStatusCode.Unauthorized) {
call.respond(HttpStatusCode.Unauthorized, "Your Response Object")
}
}
for more informatin please read these links:
https://ktor.io/docs/status-pages.html
https://github.com/ktorio/ktor/issues/366
The message when the token expires can be shown using StatusPages or by using the challenge method in the JWTAuth class like this:
jwt {
challenge { _, _ ->
call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
}
}

Redirect_URI error when using GoogleAuth.grantOfflineAccess to authenticate on server

I'm trying to use the authorization flow outlined at https://developers.google.com/identity/sign-in/web/server-side-flow.
I've created the credentials as indicated... with no Authorized redirect URIs specified as the doc indicates: "The Authorized redirect URI field does not require a value. Redirect URIs are not used with JavaScript APIs."
The code initiating the authorization is:
Client button and callback:
<script>
$('#signinButton').click(function() {
var auth2 = gapi.auth2.getAuthInstance();
auth2.grantOfflineAccess().then(signInCallback);
});
function signInCallback(authResult) {
console.log('sending to server');
if (authResult['code']) {
// Send the code to the server
$.ajax({
type: 'POST',
url: 'CheckAuth',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
contentType: 'application/octet-stream; charset=utf-8',
success: function(result) {
// Handle or verify the server response.
},
processData: false,
data: authResult['code']
});
} else {
// There was an error.
}
}
</script>
Server side (CheckAuth method to create credentials from auth code, which it receives correctly via the javascript callback):
private Credential authorize() throws Exception {
// load client secrets
InputStream is = new FileInputStream(clientSecretsPath_);
InputStreamReader isr = new InputStreamReader(is);
GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, isr);
String redirect_URI = "";
GoogleTokenResponse tokenResponse =
new GoogleAuthorizationCodeTokenRequest(
httpTransport, JSON_FACTORY,
"https://www.googleapis.com/oauth2/v4/token",
clientSecrets.getDetails().getClientId(),
clientSecrets.getDetails().getClientSecret(),
token_,
redirect_URI)
.execute();
String accessToken = tokenResponse.getAccessToken();
// Use access token to call API
GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
return credential;
}
The flow works correctly, up until the point my server attempts to exchange the authorization code for the token response (GoogleAuthorizationCodeTokenRequest.execute() )... the auth server returns:
400 Bad Request
{
"error" : "invalid_request",
"error_description" : "Missing parameter: redirect_uri"
}
Given the error, I looked in debug at the auth instance in javascript and noted what it indicated was the redirect_uri. I then updated my google credentials and specified that URI in the Authorized redirect URIs (it's the URL that accessed the javascript, as the auth server correctly returns to the specified javascript callback). With the updated credentials and the URI specified in the instantiation of GoogleAuthorizationCodeTokenRequest (String redirect_URI = "http://example.com:8080/JavascriptLocation";), the error then becomes:
400 Bad Request
{
"error" : "redirect_uri_mismatch",
"error_description" : "Bad Request"
}
I've tracked all the way through to the actual HttpRequest to the auth server (www.googleapis.com/oauth2/v4/token) and cannot tell what redirect_uri it is looking for.
Does anyone know what the value of redirect_uri should be in this case (when using grantOfflineAccess())? I'm happy to post more of the code, if that is at all helpful... just didn't want to flood the page. Thanks.
Found a reference to "postmessage" right after posting the question... using it as the redirect_URI on the server side seems to generate a successful response from the auth server. So... setting redirect_URI="postmessage" in the code below appears to work in this situation.
GoogleTokenResponse tokenResponse =
new GoogleAuthorizationCodeTokenRequest(
httpTransport, JSON_FACTORY,
"https://www.googleapis.com/oauth2/v4/token",
clientSecrets.getDetails().getClientId(),
clientSecrets.getDetails().getClientSecret(),
token_,
redirect_URI)
.execute();

Backbone.js and user authentication

I have been wondering for quite a while how I would go about authenticating users using Backbone because I have been reading a few articles about it and a lot of them are talking about tokens and keys.. But I just want to be able to sign in a user and register a user like you would normally.
I was thinking that on the web app start up there would be a request to the route '/me' and then the server gives the user back appropriate information if he/she is logged in.
Like if the route came back with {loggedIn: false} the backbone router would send the user to the login/register pages only. But if it came back with a users profile information then it would obviously mean he had a session.
Is this an okay way of going back user authentication when using Backbone?
Short answer: wire up $.ajax to respond to 401 (Unauthorized) status codes.
Long answer: We're consuming a RESTful api from a single page website. when the server detects an unauthorized request, it just returns a 401. The client will redirect to /login?#requested/resource.
/login will prompt for authorization (redirect to google's oath server in our case) then add an authorization cookie and redirect to the originally requested #requested/resource
we're also sending the auth cookie on every $.ajax request.
Hopefully this is helpful.
define(
[
'jquery',
'jquery.cookie'
],
function ($) {
var redirectToLogin = function () {
var locationhref = "/login";
if (location.hash && location.hash.length > 0) {
locationhref += "?hash=" + location.hash.substring(1);
}
location.href = locationhref;
};
var $doc = $(document);
$doc.ajaxSend(function (event, xhr) {
var authToken = $.cookie('access_token');
if (authToken) {
xhr.setRequestHeader("Authorization", "Bearer " + authToken);
}
});
$doc.ajaxError(function (event, xhr) {
if (xhr.status == 401)
redirectToLogin();
});
});