GAE: Is there a way to authorize version endpoints with GCP OAuth redirect URLs? - google-app-engine

I thought I was smart by using a custom version tag for my applications so I could predetermine the fully qualified version-URL. And my integration tests work wonders when I do this, however, I can't figure out a way around my OAuth rules.
Currently, wildcard Authorized redirect URIs aren't allowed:
What I want to achieve is basically a fully functional app (consist of three services) that has yet to be promoted. That way our testers can greenlight to deployment before the deployment.
Anyone got any idea is such a thing is possible?

So thanks to #sllopis's comment I ended up creating a login service that took care of the redirecting to multiple versions of the same service.
It isn't the perfect solution, but it is one we can use. OAuth to our unpublished apps.
//#ts-check
const fastify = require("fastify")({ logger: true });
const {
google: {
auth: { OAuth2 }
}
} = require("googleapis");
const { googleLoginCallback, googleClientId, googleClientSecret } = process.env;
const defaultScope = [
"https://www.googleapis.com/auth/plus.me",
"https://www.googleapis.com/auth/userinfo.email"
];
const getGoogleRedirecturl = () =>
new OAuth2(
googleClientId,
googleClientSecret,
googleLoginCallback
).generateAuthUrl({
access_type: "offline",
prompt: "consent",
scope: defaultScope
});
fastify.get("/login", (req, reply) => {
const { versionEndpoint } = req.query;
const encodedEndpoint = encodeURIComponent(versionEndpoint);
const query = `&state=${encodedEndpoint}`;
reply.redirect(getGoogleRedirecturl() + query);
});
fastify.get("/login/callback", (req, reply) => {
const { code, state } = req.query;
const endpoint = decodeURIComponent(state);
reply.redirect(endpoint + "?code=" + code);
});
fastify
.listen(3000)
.then(str => {
fastify.log.info(`server listening on ${fastify.server.address()}`, str);
})
.catch(err => {
fastify.log.error(err);
process.exit(1);
});

Related

What Is The Right Way to Call A Google Cloud Function via A Next.js (with Typescript) App?

The component that I have doing an API call looks like this:
import React from 'react';
import { Button } from 'react-bootstrap';
class Middle extends React.Component {
handleClick() {
console.log('this is:', this);
}
// This syntax ensures `this` is bound within handleClick. // Warning: this is *experimental* syntax. handleClick = () => { console.log('this is:', this); }
async fetchNonce() {
const response = await fetch("http://localhost:<PORT NUMBER HERE>/path/to/endpoint")
console.log(response.status)
const data = await response.json()
console.log(data)
}
render() {
return (
<div className="col-md-12 text-center">
<Button variant='primary' onClick={this.fetchNonce}>
Click me
</Button>
</div>
);
}
}
export default Middle;
The above code works for a simple throw away local node that I'm running for testing purposes. I want to use the above code to, instead of running a local function, reach out to Google Cloud Functions and run one of my Google Cloud Functions instead.
I created a Google Cloud Platform account, and made a simple hello_world type Python "Cloud Function" that just spits out some simple text. I made this function "HTTP" accessible and only able to be called/authenticated by a "Service Account" that I made for the purpose of calling this very function. I generated a key for this "Service Account" and downloaded the json file for the key.
I've seen a lot of tutorials on how to call functions via an authenticated "Service Account" but I'm concerned that a lot of methods might not be secure or following best practices. What is the best practices way to modify the above code to call a Google Cloud Function and authenticate with my "Service Account?"
As mentioned in the thread Ans1 and Ans2:
1. You can set project-wide or per-function permissions outside the function(s), so that only authenticated users can cause the
function to fire, even if they try to hit the endpoint. Here's Google
Cloud Platform documentation on setting permissions and
authenticating users. Note that, as of writing, I believe using
this method requires users to use a Google account to authenticate.
2. JWT token passed in in the form of an Authorization header access token. implementation in Node:
const client = jwksClient({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: "https://<auth0-account>.auth0.com/.well-known/jwks.json"}); .
function verifyToken(token, cb) {
let decodedToken;
try {
decodedToken = jwt.decode(token, {complete: true});
} catch (e) {
console.error(e);
cb(e);
return;
}
client.getSigningKey(decodedToken.header.kid, function (err, key) {
if (err) {
console.error(err);
cb(err);
return;
}
const signingKey = key.publicKey || key.rsaPublicKey;
jwt.verify(token, signingKey, function (err, decoded) {
if (err) {
console.error(err);
cb(err);
return
}
console.log(decoded);
cb(null, decoded);
});
});
}
function checkAuth (fn) {
return function (req, res) {
if (!req.headers || !req.headers.authorization) {
res.status(401).send('No authorization token found.');
return;
}
const parts = req.headers.authorization.split(' ');
if (parts.length != 2) {
res.status(401).send('Bad credential format.');
return;
}
const scheme = parts[0];
const credentials = parts[1];
if (!/^Bearer$/i.test(scheme)) {
res.status(401).send('Bad credential format.');
return;
}
verifyToken(credentials, function (err) {
if (err) {
res.status(401).send('Invalid token');
return;
}
fn(req, res);
});
};
}
Google Cloud Functions now support two types of authentication and
authorization: Identity and Access Management (IAM) and OAuth 2.0.
Documentation can be found here.
To gather more information about cloud functions with a JWT token passed in in the form of an Authorization header access token, you can refer to the documentation.

Logout from next-auth with keycloak provider not works

I have a nextjs application with next-auth to manage the authentication.
Here my configuration
....
export default NextAuth({
// Configure one or more authentication providers
providers: [
KeycloakProvider({
id: 'my-keycloack-2',
name: 'my-keycloack-2',
clientId: process.env.NEXTAUTH_CLIENT_ID,
clientSecret: process.env.NEXTAUTH_CLIENT_SECRET,
issuer: process.env.NEXTAUTH_CLIENT_ISSUER,
profile: (profile) => ({
...profile,
id: profile.sub
})
})
],
....
Authentication works as expected, but when i try to logout using the next-auth signOut function it doesn't works. Next-auth session is destroyed but keycloak mantain his session.
After some research i found a reddit conversation https://www.reddit.com/r/nextjs/comments/redv1r/nextauth_signout_does_not_end_keycloak_session/ that describe the same problem.
Here my solution.
I write a custom function to logout
const logout = async (): Promise<void> => {
const {
data: { path }
} = await axios.get('/api/auth/logout');
await signOut({ redirect: false });
window.location.href = path;
};
And i define an api path to obtain the path to destroy the session on keycloak /api/auth/logout
export default (req, res) => {
const path = `${process.env.NEXTAUTH_CLIENT_ISSUER}/protocol/openid-connect/logout?
redirect_uri=${encodeURIComponent(process.env.NEXTAUTH_URL)}`;
res.status(200).json({ path });
};
UPDATE
In the latest versions of keycloak (at time of this post update is 19.*.* -> https://github.com/keycloak/keycloak-documentation/blob/main/securing_apps/topics/oidc/java/logout.adoc) the redirect uri becomes a bit more complex
export default (req, res) => {
const session = await getSession({ req });
let path = `${process.env.NEXTAUTH_CLIENT_ISSUER}/protocol/openid-connect/logout?
post_logout_redirect_uri=${encodeURIComponent(process.env.NEXTAUTH_URL)}`;
if(session?.id_token) {
path = path + `&id_token_hint=${session.id_token}`
} else {
path = path + `&client_id=${process.env.NEXTAUTH_CLIENT_ID}`
}
res.status(200).json({ path });
};
Note that you need to include either the client_id or id_token_hint parameter in case that post_logout_redirect_uri is included.
So, I had a slightly different approach building upon this thread here.
I didn't really like all the redirects happening in my application, nor did I like adding a new endpoint to my application just for dealing with the "post-logout handshake"
Instead, I added the id_token directly into the initial JWT token generated, then attached a method called doFinalSignoutHandshake to the events.signOut which automatically performs a GET request to the keycloak service endpoint and terminates the session on behalf of the user.
This technique allows me to maintain all of the current flows in the application and still use the standard signOut method exposed by next-auth without any special customizations on the front-end.
This is written in typescript, so I extended the JWT definition to include the new values (shouldn't be necessary in vanilla JS
// exists under /types/next-auth.d.ts in your project
// Typescript will merge the definitions in most
// editors
declare module "next-auth/jwt" {
interface JWT {
provider: string;
id_token: string;
}
}
Following is my implementation of /pages/api/[...nextauth.ts]
import axios, { AxiosError } from "axios";
import NextAuth from "next-auth";
import { JWT } from "next-auth/jwt";
import KeycloakProvider from "next-auth/providers/keycloak";
// I defined this outside of the initial setup so
// that I wouldn't need to keep copying the
// process.env.KEYCLOAK_* values everywhere
const keycloak = KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
issuer: process.env.KEYCLOAK_ISSUER,
});
// this performs the final handshake for the keycloak
// provider, the way it's written could also potentially
// perform the action for other providers as well
async function doFinalSignoutHandshake(jwt: JWT) {
const { provider, id_token } = jwt;
if (provider == keycloak.id) {
try {
// Add the id_token_hint to the query string
const params = new URLSearchParams();
params.append('id_token_hint', id_token);
const { status, statusText } = await axios.get(`${keycloak.options.issuer}/protocol/openid-connect/logout?${params.toString()}`);
// The response body should contain a confirmation that the user has been logged out
console.log("Completed post-logout handshake", status, statusText);
}
catch (e: any) {
console.error("Unable to perform post-logout handshake", (e as AxiosError)?.code || e)
}
}
}
export default NextAuth({
secret: process.env.NEXTAUTH_SECRET,
providers: [
keycloak
],
callbacks: {
jwt: async ({ token, user, account, profile, isNewUser }) => {
if (account) {
// copy the expiry from the original keycloak token
// overrides the settings in NextAuth.session
token.exp = account.expires_at;
token.id_token = account.id_token;
}
return token;
}
},
events: {
signOut: ({ session, token }) => doFinalSignoutHandshake(token)
}
});
signOut only clears session cookies without destroying user's session on the provider.
Year 2023 Solution:
hit GET /logout endpoint of the provider to destroy user's session
do signOut() to clear session cookies, only if step 1 was successful
Implementation:
Assumption: you are storing user's idToken in the session object returned by useSession/getSession/getServerSession
create an idempotent endpoint (PUT) on server side to make this GET call to the provider
create file: pages/api/auth/signoutprovider.js
import { authOptions } from "./[...nextauth]";
import { getServerSession } from "next-auth";
export default async function signOutProvider(req, res) {
if (req.method === "PUT") {
const session = await getServerSession(req, res, authOptions);
if (session?.idToken) {
try {
// destroy user's session on the provider
await axios.get("<your-issuer>/protocol/openid-connect/logout", { params: id_token_hint: session.idToken });
res.status(200).json(null);
}
catch (error) {
res.status(500).json(null);
}
} else {
// if user is not signed in, give 200
res.status(200).json(null);
}
}
}
wrap signOut by a function, use this function to sign a user out throughout your app
import { signOut } from "next-auth/react";
export async function theRealSignOut(args) {
try {
await axios.put("/api/auth/signoutprovider", null);
// signOut only if PUT was successful
return await signOut(args);
} catch (error) {
// <show some notification to user asking to retry signout>
throw error;
}
}
Note: theRealSignOut can be used on client side only as it is using signOut internally.
Keycloak docs logout

How to use NextJS redirects for case insensitive routes?

I'm rewriting a complete site that worked with case insensative routes. That is, someone can type:
https://www.siliconvalley-codecamp.com/Presenter/2019/Douglas-Crockford-1124
or
https://www.siliconvalley-codecamp.com/presenter/2019/douglas-crockford-1124
and they both resolve to the same page. In NextJS 11, I currently am using a file in my pages folder as follows to resolve this:
/pages/presenter[[..slug]].tsx which means that the first URL I mention above will not work, but the second one will. I'm using ISG also so that even the slug does not handle routing correctly.
Currently, the site is built here and you can see the behavior problem.
working:
https://svcc.mobi/presenter/2019/douglas-crockford-1124
Not working:
https://svcc.mobi/Presenter/2019/Douglas-Crockford-1124
I've looked at the docs here: https://nextjs.org/docs/api-reference/next.config.js/rewrites but don't see a way to handle the general case of a route with a slug.
You are using the lowercase file name.
/pages/presenter[[..slug]].tsx
In fact, you need 2 files. lowercase and uppercase. To solve this issue, you can use the server-side rendering
on root directory server.ts or server.js
import express, { Request, Response } from "express";
import next from "next";
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const port = process.env.PORT || 3000;
(async () => {
try {
await app.prepare();
const server = express();
server.get("/p/:id", (req: Request, res: Response) => {
app.render(req, res, "/post", req.params);
});
server.all("*", (req: Request, res: Response) => {
return handle(req, res);
});
server.listen(port, (err?: any) => {
if (err) throw err;
console.log(`> Ready on localhost:${port} - env ${process.env.NODE_ENV}`);
});
} catch (e) {
console.error(e);
process.exit(1);
}
})();
This is an example code for the server-side rendering.
I think you can render a component with this code
server.get("/p/:id", (req: Request, res: Response) => {
app.render(req, res, "/post", req.params);
});
If you need more info, please refer this documentation
https://nextjs.org/docs/basic-features/pages#server-side-rendering

reCAPTCHA first attempt causes it to reset. Firebase phoneAuthVerifier

Hello I'm having issues implementing recaptcha into our firebase project. I'm using it with firebase.auth.PhoneAuthProvider, to initially enroll a second factor and then use the recaptcha for multi-factor authentication on login.
I've used the https://cloud.google.com/identity-platform/docs/web/mfa doc for the implementation and it does work but it resets on the first successful attempt. The tickbox gets checked but instantly the whole recaptcha rerenders/is reset.
https://imgur.com/PJCJujo (here is a tiny video of that reset behaviour)
The fact that it is rerendering makes me think it is an error in the implementation and possibly to do with React but I am struggling to solve it.
Login Function
const frontendLogin = () => {
const { email, password } = registerDetails
firebaseApp.auth().signInWithEmailAndPassword(email, password)
.then(data => {
// if (data.user.emailVerified) {
axios.get(`/users/${data.user.uid}`)
.then(res => {
// Handle login for users not enrolled with multi-factor
})
.catch(error => {
console.log(error)
if (error.code === 'auth/multi-factor-auth-required') {
setResolver(error.resolver)
setHints(error.resolver.hints[0])
setPhoneVerifyRequired(true)
// Ask user which second factor to use.
if (error.resolver.hints[selectedIndex].factorId ===
firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
const phoneInfoOptions = {
multiFactorHint: error.resolver.hints[selectedIndex],
session: error.resolver.session
};
const phoneAuthProvider = new firebase.auth.PhoneAuthProvider(appAuth);
const recaptchaVerifier = new firebase.auth.RecaptchaVerifier(
captchaRef.current,
{
'size': 'normal',
'callback': function (response) {
console.log('captcha!')
handleSolved()
},
'expired-callback': function () {
console.log('captcha expired')
},
'hl': locale
},
firebaseApp);
// Send SMS verification code
return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
.then(function (verificationId) {
setVerificationId(verificationId)
}).catch(err => console.log(err))
} else {
// Unsupported second factor.
}
} else {
console.log(error)
}
})
}
const handleSolved = () => {
setCaptchad(true)
setIsLoading(false)
}
const handleRecaptcha = () => {
var cred = firebase.auth.PhoneAuthProvider.credential(
verificationId, verificationCode);
console.log(cred)
var multiFactorAssertion =
firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
// Complete sign-in.
resolver.resolveSignIn(multiFactorAssertion)
.then(function (data) {
// User successfully signed in with the second factor phone number.
// Handle login for users enrolled with multi-factor
})
}
I am unsure as to why this approach isn't working but I believe it may be to do with React? I have looked into using a React Recaptcha package but as the docs were just written in javascript I've just tried to implement it in this way.
I haven't deviated too far from the docs only to set state with elements including the verification ID that is returned from the successful recaptcha etc.
I also appreciate I am setting state too much and need to refactor the state at the top of the component but as I'm still fleshing this component out it hasn't been refactored yet so I apologise it's a bit messy!
If anyone has any pointers that would be great or if I need to provide more code then let me know, any help is appreciated!

React Application Update Data from DynamoDB Change

I am building a React application with GraphQL using AWS AppSync with DynamoDB. My use case is that I have a table of data that is being pulled from a DynamoDB table and displayed to the user using GraphQL. I have a few fields that are being updated by step functions running on AWS. I need those fields to be automatically updated for the user much like a subscription from GraphQL would do but I found out that subscriptions are tied to mutations and thus an update to the database from step functions will not trigger a subscription update on the frontend. To get around this I am using the following:
useEffect(() => {
setTimeout(getSubmissions, 5 * 1000)
})
Obviously this is a lot of overfetching and will probably incur unnecessary expense. I have looked for a better solution and come across DynamoDB streams but DynamoDB streams can't help me if they can't trigger the frontend to refresh the component. There has to be a better solution than what I have come up with.
Thanks!
You are correct, in AWS AppSync, to trigger a subscription publish you must trigger a GraphQL mutation.
but I found out that subscriptions are tied to mutations and thus an
update to the database from step functions will not trigger a
subscription update on the frontend.
If you update your DynamoDB table directly via step functions or via DynamoDB streams, then AppSync has no way to know the data refreshed.
Why don't you have your step function use an AppSync mutation instead of updating your table directly? That way you can link a subscription to the mutation and have your interested clients get pushed updates when the data is refreshed.
Assuming you are using Cognito as your authentication for your AppSync application, you could set a lambda trigger on the dynamo table that generates a cognito token, and uses that make an authorized request to your mutation endpoint. NOTE: in your cognito userpool>app clients page, you will need to check the Enable username password auth for admin APIs for authentication (ALLOW_ADMIN_USER_PASSWORD_AUTH) box to generate a client secret.
const AWS = require('aws-sdk');
const crypto = require('crypto');
var jwt = require('jsonwebtoken');
const secrets = require('./secrets.js');
var cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
var config;
const adminAuth = () => new Promise((res, rej) => {
const digest = crypto.createHmac('SHA256', config.SecretHash)
.update(config.userName + config.ClientId)
.digest('base64');
var params = {
AuthFlow: "ADMIN_NO_SRP_AUTH",
ClientId: config.ClientId, /* required */
UserPoolId: config.UserPoolId, /* required */
AuthParameters: {
'USERNAME': config.userName,
'PASSWORD': config.password,
"SECRET_HASH":digest
},
};
cognitoidentityserviceprovider.adminInitiateAuth(params, function(err, data) {
if (err) {
console.log(err.stack);
rej(err);
}
else {
data.AuthenticationResult ? res(data.AuthenticationResult) : rej("Challenge requested, to verify, login to app using admin credentials");
}
});
});
const decode = auth => new Promise( res => {
const decoded = jwt.decode(auth.AccessToken);
auth.decoded = decoded
res(auth);
});
//example gql query
const testGql = auth => {
const url = config.gqlEndpoint;
const payload = {
query: `
query ListMembers {
listMembers {
items{
firstName
lastName
}
}
}
`
};
console.log(payload);
const options = {
headers: {
"Authorization": auth.AccessToken
},
};
console.log(options);
return axios.post(url, payload, options).then(data => data.data)
.catch(e => console.log(e.response.data));
};
exports.handler = async (event, context, callback) => {
await secrets() //some promise that returns your keys object (i use secrets manager)
.then( keys => {
#keys={ClientId:YOUR_COGNITO_CLIENT,
# UserPoolId:YOUR_USERPOOL_ID,
# SecretHash:(obtained from cognito>userpool>app clients>app client secret),
# gqlEndpoint:YOUR_GRAPHQL_ENDPOINT,
# userName:YOUR_COGNITO_USER,
# password:YOUR_COGNITO_USER_PASSWORD,
# }
config = keys
return adminAuth()
})
.then(auth => {
return decode(auth)
})
.then(auth => {
return testGql(auth)
})
.then( data => {
console.log(data)
callback(null, data)
})
.catch( e => {
callback(e)
})
};

Resources