authenticate a server application with an OAuth2 flow - google-api

Probleme
I want to create a server application that modifies playlists on spotify, deezer and youtube (to create multi-platform playlists). My problem is that these different services require OAuth2 authentication to manipulate playlists, and that this authentication requires human intervention
What I've tried
I tried to use the api key, but it did not allow me to edit the playlist (access_key needed).
My Research
from what I understood from the different documentation, the api key is made for server applications (like my case) and that the OAuth2 authentication is more made to authenticate a client.
Question
how to authenticate a server side application with an OAuth2 flow. or how to allow a server application to modify/manipulate playlists on youtube, spotify, deezer.
Details
i use node server.

The first thing you need to under stand is the difference between public and private data. Private data is data that is not owned by anyone that any one has access to. Public videos on YouTube for example can be read by anyone. Private user data is data that is owned by a user that being their play lists for example.
API keys will only give you access to public data not private user data.
The only flow available to you to read YouTube private user data is Ouath2. What you should do is follow the official node.js sample.
When you run this sample it is single user it will store the user credentials in TOKEN_DIR. Then it will use the refresh token that was generated in order to request a new access token when ever it needs one. So you will need to authorize your code once then it will be able to run on your server without you having to grant it permissions.
var fs = require('fs');
var readline = require('readline');
var {google} = require('googleapis');
var OAuth2 = google.auth.OAuth2;
// If modifying these scopes, delete your previously saved credentials
// at ~/.credentials/youtube-nodejs-quickstart.json
var SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'];
var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
process.env.USERPROFILE) + '/.credentials/';
var TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json';
// Load client secrets from a local file.
fs.readFile('client_secret.json', function processClientSecrets(err, content) {
if (err) {
console.log('Error loading client secret file: ' + err);
return;
}
// Authorize a client with the loaded credentials, then call the YouTube API.
authorize(JSON.parse(content), getChannel);
});
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
*
* #param {Object} credentials The authorization client credentials.
* #param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
var clientSecret = credentials.installed.client_secret;
var clientId = credentials.installed.client_id;
var redirectUrl = credentials.installed.redirect_uris[0];
var oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, function(err, token) {
if (err) {
getNewToken(oauth2Client, callback);
} else {
oauth2Client.credentials = JSON.parse(token);
callback(oauth2Client);
}
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
*
* #param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
* #param {getEventsCallback} callback The callback to call with the authorized
* client.
*/
function getNewToken(oauth2Client, callback) {
var authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES
});
console.log('Authorize this app by visiting this url: ', authUrl);
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Enter the code from that page here: ', function(code) {
rl.close();
oauth2Client.getToken(code, function(err, token) {
if (err) {
console.log('Error while trying to retrieve access token', err);
return;
}
oauth2Client.credentials = token;
storeToken(token);
callback(oauth2Client);
});
});
}
/**
* Store token to disk be used in later program executions.
*
* #param {Object} token The token to store to disk.
*/
function storeToken(token) {
try {
fs.mkdirSync(TOKEN_DIR);
} catch (err) {
if (err.code != 'EEXIST') {
throw err;
}
}
fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
if (err) throw err;
console.log('Token stored to ' + TOKEN_PATH);
});
}
/**
* Lists the names and IDs of up to 10 files.
*
* #param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
function getChannel(auth) {
var service = google.youtube('v3');
service.channels.list({
auth: auth,
part: 'snippet,contentDetails,statistics',
forUsername: 'GoogleDevelopers'
}, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
var channels = response.data.items;
if (channels.length == 0) {
console.log('No channel found.');
} else {
console.log('This channel\'s ID is %s. Its title is \'%s\', and ' +
'it has %s views.',
channels[0].id,
channels[0].snippet.title,
channels[0].statistics.viewCount);
}
});
}
You may want to monitor it there are some instances where a refresh token will be expired in the event that happens you will need to authorize the application again.

An OAuth 2 application delegates the authentication to services (eg. Google, Spotify, Amazon, Github, LinkedIn, etc.) that host a user account and asks for (limited) authorization from those services, after the user has given consent.
There is a difference between authentication and authorization.
Authentication makes sure a user is who they're claiming to be. Whereas Authorization governs what the user has access to.
So your OAuth2 application is leveraging Spotify social account integration to delegate authentication to Spotify and in turn receiving limited authorization to modify playlists (only).
To answer your question: To be able to perform any action (eg. modify Spotify playlists) you'd need the access token from Spotify API. Here's what the process looks like:
User logs in to your website using their Spotify account.
User is redirected to the consent screen. They give approval for the permissions your application is asking for (i.e., modify playlist permission).
Once the user consents, your application receives an access token (along with a refresh token, and profile information).
Your application then uses the provided access token to modify the user playlists.
Passportjs is a very well built library around OAuth2 and it has support for Spotify OAuth2. I have recently documented about the complete process to implement multi-provider OAuth2 in node.js along with the explanations.

Related

How can we reset a Cognito User's password without using Cognito's forgot password flow?

I am working on a serverless project using node.js and AWS Lambda.
For auth, I am using AWS Cognito. (Frontend is a web-app in Vue.js on AWS Amplify).
I would like to write my own implementation of resetting a user's password who has forgotten their password.
Basically, the end-user fills up a form with their email. If email is in the system, I send them a reset link (which has a unique code I set in the DB).
I am aware of Cognito's Forgot Password flow and also a solution in which I can capture Cognito's "email sending" code and over-ride the email with my own template passing the code in the URL mentioned here.
I stumbled upon the adminSetUserPassword API which I was sure would work -- but no matter what I do, my lambda function does not get permissions to execute this operation.
This is my nodejs code:
import AWS from 'aws-sdk';
const COGNITO_POOL_ID = process.env.COGNITO_USERPOOL_ID;
const csp = new AWS.CognitoIdentityServiceProvider();
export async function resetUserPassword(username, newPassword) {
// Constructing request to send to Cognito
const params = {
Password: newPassword,
UserPoolId: COGNITO_POOL_ID,
Username: username,
Permanent: true,
};
await csp.adminSetUserPassword(params).promise();
return true;
}
This is my IAM permission for the lambda function (it is in serverless yml format):
CognitoResetPasswordIAM:
Effect: Allow
Action:
- cognito-idp:*
Resource:
- arn:aws:cognito-idp:us-east-1::*
(I will fine-tune the permissions once this works)
The following is the error message I am getting.
I am starting to feel that my approach to doing this is not the recommended way of doing things.
User: arn:aws:sts::[XXXXXXX]:assumed-role/[YYYYYYYYY]-us-east-1-lambdaRole/web-app-service-dev-resetPassword is not authorized to perform: cognito-idp:AdminSetUserPassword on resource: arn:aws:cognito-idp:us-east-1:[[XXXXXXX]]:userpool/us-east-1_ZZZZZZZZ
(Serverless has access to my AWS Access key with * permissions on * resources -- so I don't think I am missing any permissions there).
My questions:
Is this the recommended way of doing this?
Is it possible for me to configure permissions in a way that my lambda functions have the required permissions to perform this operation?
It turns out, you need to use the Amplify API and not the Cognito API.
This involves a couple of steps:
1. Configure your Cognito Amplify Service for Auth.
import Amplify, { Auth } from 'aws-amplify';
export function configureCognitoAuth() {
Amplify.configure({
Auth: {
region: process.env.COGNITO_REGION,
userPoolId: process.env.COGNITO_USERPOOL_ID,
mandatorySignIn: false,
userPoolWebClientId: process.env.COGNITO_CLIENT_ID,
authenticationFlowType: 'USER_PASSWORD_AUTH',
oauth: {
domain: process.env.COGNITO_APP_DOMAIN,
scope: ['phone', 'email', 'profile', 'openid', 'aws.cognito.signin.user.admin'],
responseType: 'code', // or 'token', note that REFRESH token will only be generated when the responseType is code
},
},
});
// You can get the current config object
Auth.configure();
}
2. Call the Auth.forgotPassword service to send the actual password here
import { Auth } from 'aws-amplify';
async function sendUserPasswordResetEmail(event) {
// Any validation checks, rate limits you want to check here, etc.
try {
configureCognitoAuth();
await Auth.forgotPassword(userId);
} catch (error) {
// An error occurred while sending the password reset email
}
}
3. Write a forgotPasswordEmailTrigger Cognito Hook
This replaces the default Cognito Reset password email with your own custom email.
This is also a lamdba method which you need to attach to the Cognito Custom Message trigger (from Cognito > General Settings > Triggers)
My code for this looks like so:
async function forgotPasswordEmailTrigger(event, context, callback) {
// Confirm it is a PreSignupTrigger
if (event.triggerSource === 'CustomMessage_ForgotPassword') {
const { userName } = event;
const passwordCode = event.request.codeParameter;
const resetUrl = `${BASE_URL}/password_reset/${userName}/${passwordCode}`;
let message = 'Your HTML email template goes here';
message = message
.replace(/{{passwordResetLink}}/g, resetUrl);
event.response.emailSubject = 'Email Subject here';
event.response.emailMessage = message;
}
// Return to Amazon Cognito
callback(null, event);
}
The event.request.codeParameter is where the code is returned from Cognito. I think there is a way to change this, but I didn't bother. I use the same code to verify in the next step.
4. Call the forgotPasswordSubmit method from the Amplify Auth service when a password reset request is sent to your backend
When the user clicks the URL, they come to the website and I pick up the code and the userID from the URL (from Step 3) and then verify the code + reset the password like so:
async function resetPassword(event) {
const { token, password, user_id } = event.body;
// Do your validations & checks
// Getting to here means everything is in order. Reset the password
try {
configureCognitoAuth(); // See step 1
await Auth.forgotPasswordSubmit(user_id, token, password);
} catch (error) {
// Error occurred while resetting the password
}
const result = {
result: true,
};
return {
statusCode: 200,
body: JSON.stringify(result),
};
}

Google authenticated HTTP client with dart

I have have run the examples from https://github.com/dart-lang/googleapis_examples/blob/master/drive_upload_download_console/bin/main.dart.
The example creates an authenticated HTTP client for accessing Google Drive API with:
import 'package:googleapis_auth/auth_io.dart' as auth;
…
…
auth.clientViaUserConsent(identifier, scopes, userPrompt).then((client) { // with client_id, client_secret, scope
var api = new drive.DriveApi(client);
…
…
}
When I run the example I have to give the User consent in a web browser each time I run the example above.
I want to create an authenticated HTTP client without having to use the user consent fonction (auth.clientViaUserConsent) but with a stored acces token or the refresh token. How can I create such an authenticated HTTP client? With the googleapis_auth package?(https://pub.dartlang.org/packages/googleapis_auth)
You are already there.
Your client object contains all you need already.
Here is your modified code using the stored credentials to inquire about freeBusy time:
auth.clientViaUserConsent(identifier, scopes, userPrompt).then((client) { // with client_id, client_secret, scope
var api = new drive.DriveApi(client);
debugPrint(' access token: ' + client.credentials.accessToken.data +' refresh token ' + client.credentials.refreshToken);
// store the tokens in the apps key store
}
At some time in the future make a new call to obtain new access credentials from the never expiring refresh token, and create a new client for your purposes.
AccessCredentials _fromStorage = AccessCredentials(client.credentials.accessToken,
client.credentials.refreshToken, _scopes );
var _newClient = new http.Client();
AccessCredentials _accessCredentials = await refreshCredentials( _clientID, _fromStorage , _newClient);
_newClient = authenticatedClient(_newClient, _accessCredentials);
// the code below was just for me to test this out with my API scopes. replace with your code
var calendar = cal.CalendarApi(_newClient);
String calendarId = "---some string---";
cal.FreeBusyRequest _request = cal.FreeBusyRequest.fromJson(
{
'items': [
{'id': calendarId, 'busy': 'Active'}
],
'timeMin': (new DateTime(2020, 11, 17)).toIso8601String()+'Z',
'timeMax': (new DateTime(2020, 11, 19)).toIso8601String()+'Z'
});
debugPrint('request: ' + _request.toJson().toString());
cal.FreeBusyResponse response = await calendar.freebusy.query(_request);
debugPrint(response.toJson().toString());
});

Invalid signature while validating Azure ad access token, but id token works

I am getting invalid signature while using jwt.io to validate my azure ad access token. My id token, however, validates just fine!
I have seen and tried the solutions suggested in
Invalid signature while validating Azure ad access token
and
https://nicksnettravels.builttoroam.com/post/2017/01/24/Verifying-Azure-Active-Directory-JWT-Tokens.aspx
but neither works for my access token.
The access and Id token is generated via Adal.js:
var endpoints = {
"https://graph.windows.net": "https://graph.windows.net"
};
var configOptions = {
tenant: "<ad>.onmicrosoft.com", // Optional by default, it sends common
clientId: "<app ID from azure portal>",
postLogoutRedirectUri: window.location.origin,
endpoints: endpoints,
}
window.authContext = new AuthenticationContext(configOptions);
Why can I validate my ID token, but not my access token?
Please refer to thread : https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/609
but if look at the Jwt.Header you will see a 'nonce'. This means you need special processing. Normal processing will fail.
So if nonce includes in access token , validate signature with JWT.io or JwtSecurityToken won't success .
If anyone else has invalid signature errors, you should check this comment : https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/521#issuecomment-577400515
Solved the issue for my configuration.
Essentially, if you are getting access tokens to access your own resource server and not the Graph API, your scopes parameter should be [CLIENT_ID]/.default (and if you are using the access token to access the Graph API, you don't need to validate the token yourself)
Thanks to Nan Yu I managed to get token that can be validated by any public jwt validator like jwt.io
(couldn't put my comment in the comments section under Nan Yu's answer because its too long).
So as I understand the point from the discussion mentioned by Nan Yu that by default Azure AD generates tokens for Microsoft Graph and these tokens use special signing mechanism so that it is not possible to validate signature using public validators (except jwt.ms Microsoft's validator which most probably knows what mysterious special handling means :) ).
To get access token not for Microsoft Graph that can be validated using public validators I had to:
Remove any Microsoft Graph related scopes (by default I had only one scope configured User.Read so removed it in appConfig > API permissions)
create a custom scope for your application (appConfig > Expose an API > Add scope ...) this scope will look like api://{application-id}/scope-name
add just created scope in the application API permissions (appConfig > API permissions > Add api permission > My APIs > select your application > Delegated Permissions > Check your scope > Add permission)
then use this scope in your openid client scopes, in my case I have: openid offline_access {application-id}/scope-name
Note that in the openid client config newly created scope is used without api:// prefix (offline_access I have to enable refresh_token can be ignored if refresh token mechanism is not used)
Well thanks to #Antoine I fix my code. Here I will let my personal vue.js plugin that is working for everybody else reference:
import { PublicClientApplication } from '#azure/msal-browser'
import { Notify } from 'quasar'
export class MsalService {
_msal = null
_store = null
_loginRequest = null
constructor (appConfig, store) {
this._store = store
this._msal = new PublicClientApplication(
{
auth: {
clientId: appConfig.auth.clientId,
authority: appConfig.auth.authority
},
cache: {
cacheLocation: 'localStorage'
}
})
this._loginRequest = {
scopes: [`${appConfig.auth.clientId}/.default`]
}
}
async handleResponse (response) {
await this._store.dispatch('auth/setResponse', response)
const accounts = this._msal.getAllAccounts()
await this._store.dispatch('auth/setAccounts', accounts)
if (accounts.length > 0) {
this._msal.setActiveAccount(accounts[0])
this._msal.acquireTokenSilent(this._loginRequest).then(async (accessTokenResponse) => {
// Acquire token silent success
// Call API with token
// let accessToken = accessTokenResponse.accessToken;
await this._store.dispatch('auth/setResponse', accessTokenResponse)
}).catch((error) => {
Notify.create({
message: JSON.stringify(error),
color: 'red'
})
// Acquire token silent failure, and send an interactive request
if (error.errorMessage.indexOf('interaction_required') !== -1) {
this._msal.acquireTokenPopup(this._loginRequest).then(async (accessTokenResponse) => {
// Acquire token interactive success
await this._store.dispatch('auth/setResponse', accessTokenResponse)
}).catch((error) => {
// Acquire token interactive failure
Notify.create({
message: JSON.stringify(error),
color: 'red'
})
})
}
})
}
}
async login () {
// this._msal.handleRedirectPromise().then((res) => this.handleResponse(res))
// await this._msal.loginRedirect(this._loginRequest)
await this._msal.loginPopup(this._loginRequest).then((resp) => this.handleResponse(resp))
}
async logout () {
await this._store.dispatch('auth/setAccounts', [])
await this._msal.logout()
}
}
// "async" is optional;
// more info on params: https://quasar.dev/quasar-cli/boot-files
export default ({
app,
store,
Vue
}) => {
const msalInstance = new MsalService(
app.appConfig, store
)
Vue.prototype.$msal = msalInstance
app.msal = msalInstance
}
PD: using quasar framework
If you are using msal.js library with react, add this to your auth configuration.
scopes: [`${clientId}/.default`]
Editing scopes fixed issue for me

Removing cached Google access token that has been revoked

In my c# desktop app, I'm using Google's API to authenticate and retrieve the access token for an API. I noticed that the API will cache this token and use it again until it expires.
Using my browser, I was able to revoke the token using:
https://accounts.google.com/o/oauth2/revoke?token=1/xxxxxxx
I did this to test out how the API handles revoked tokens. As exepected, the API fails. The problem I have though is getting the API to use a new token. It continues to retrieve the cached token, even though it's been revoked. The following code is used to authenticate the use of the API:
credential = GoogleWebAuthorizationBroker.AuthorizeAsync(GoogleClientSecrets.Load(stream).Secrets, Scopes, "user", CancellationToken.None);
How can I get the API to remove the cached token and request a new one?
You have to log out before getting a new token. You can read about OAuth 2.0 here https://developers.google.com/api-client-library/dotnet/guide/aaa_oauth?hl=uk .
Below how I've done that for my windows phone 8 application:
var string const GoogleTokenFileName = "Google.Apis.Auth.OAuth2.Responses.TokenResponse-user"
public async Task LoginAsync()
{
await Logout();
_credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets
{
ClientId = _xmlfile.GoogleClientId,
ClientSecret = _xmlfile.GoogleClientSecret
}, new[] { Oauth2Service.Scope.UserinfoProfile }, "user", CancellationToken.None);
return session;
}
public async Task Logout()
{
await new WebBrowser().ClearCookiesAsync();
if (_storageService.FileExists(GoogleTokenFileName))
{
_storageService.DeleteFile(GoogleTokenFileName);
}
}
Hope it helps.

Restrict Login Email with Google OAuth2.0 to Specific Domain Name

I can't seem to find any documentation on how to restrict the login to my web application (which uses OAuth2.0 and Google APIs) to only accept authentication requests from users with an email on a specific domain name or set of domain names. I would like to whitelist as opposed to blacklist.
Does anyone have suggestions on how to do this, documentation on the officially accepted method of doing so, or an easy, secure work around?
For the record, I do not know any info about the user until they attempt to log in through Google's OAuth authentication. All I receive back is the basic user info and email.
So I've got an answer for you. In the OAuth request you can add hd=example.com and it will restrict authentication to users from that domain (I don't know if you can do multiple domains). You can find hd parameter documented here
I'm using the Google API libraries from here: http://code.google.com/p/google-api-php-client/wiki/OAuth2 so I had to manually edit the /auth/apiOAuth2.php file to this:
public function createAuthUrl($scope) {
$params = array(
'response_type=code',
'redirect_uri=' . urlencode($this->redirectUri),
'client_id=' . urlencode($this->clientId),
'scope=' . urlencode($scope),
'access_type=' . urlencode($this->accessType),
'approval_prompt=' . urlencode($this->approvalPrompt),
'hd=example.com'
);
if (isset($this->state)) {
$params[] = 'state=' . urlencode($this->state);
}
$params = implode('&', $params);
return self::OAUTH2_AUTH_URL . "?$params";
}
I'm still working on this app and found this, which may be the more correct answer to this question. https://developers.google.com/google-apps/profiles/
Client Side:
Using the auth2 init function, you can pass the hosted_domain parameter to restrict the accounts listed on the signin popup to those matching your hosted_domain. You can see this in the documentation here: https://developers.google.com/identity/sign-in/web/reference
Server Side:
Even with a restricted client-side list you will need to verify that the id_token matches the hosted domain you specified. For some implementations this means checking the hd attribute you receive from Google after verifying the token.
Full Stack Example:
Web Code:
gapi.load('auth2', function () {
// init auth2 with your hosted_domain
// only matching accounts will show up in the list or be accepted
var auth2 = gapi.auth2.init({
client_id: "your-client-id.apps.googleusercontent.com",
hosted_domain: 'your-special-domain.example'
});
// setup your signin button
auth2.attachClickHandler(yourButtonElement, {});
// when the current user changes
auth2.currentUser.listen(function (user) {
// if the user is signed in
if (user && user.isSignedIn()) {
// validate the token on your server,
// your server will need to double check that the
// `hd` matches your specified `hosted_domain`;
validateTokenOnYourServer(user.getAuthResponse().id_token)
.then(function () {
console.log('yay');
})
.catch(function (err) {
auth2.then(function() { auth2.signOut(); });
});
}
});
});
Server Code (using googles Node.js library):
If you're not using Node.js you can view other examples here: https://developers.google.com/identity/sign-in/web/backend-auth
const GoogleAuth = require('google-auth-library');
const Auth = new GoogleAuth();
const authData = JSON.parse(fs.readFileSync(your_auth_creds_json_file));
const oauth = new Auth.OAuth2(authData.web.client_id, authData.web.client_secret);
const acceptableISSs = new Set(
['accounts.google.com', 'https://accounts.google.com']
);
const validateToken = (token) => {
return new Promise((resolve, reject) => {
if (!token) {
reject();
}
oauth.verifyIdToken(token, null, (err, ticket) => {
if (err) {
return reject(err);
}
const payload = ticket.getPayload();
const tokenIsOK = payload &&
payload.aud === authData.web.client_id &&
new Date(payload.exp * 1000) > new Date() &&
acceptableISSs.has(payload.iss) &&
payload.hd === 'your-special-domain.example';
return tokenIsOK ? resolve() : reject();
});
});
};
When defining your provider, pass in a hash at the end with the 'hd' parameter. You can read up on that here. https://developers.google.com/accounts/docs/OpenIDConnect#hd-param
E.g., for config/initializers/devise.rb
config.omniauth :google_oauth2, 'identifier', 'key', {hd: 'yourdomain.com'}
Here's what I did using passport in node.js. profile is the user attempting to log in.
//passed, stringified email login
var emailString = String(profile.emails[0].value);
//the domain you want to whitelist
var yourDomain = '#google.com';
//check the x amount of characters including and after # symbol of passed user login.
//This means '#google.com' must be the final set of characters in the attempted login
var domain = emailString.substr(emailString.length - yourDomain.length);
//I send the user back to the login screen if domain does not match
if (domain != yourDomain)
return done(err);
Then just create logic to look for multiple domains instead of just one. I believe this method is secure because 1. the '#' symbol is not a valid character in the first or second part of an email address. I could not trick the function by creating an email address like mike#fake#google.com 2. In a traditional login system I could, but this email address could never exist in Google. If it's not a valid Google account, you can't login.
Since 2015 there has been a function in the library to set this without needing to edit the source of the library as in the workaround by aaron-bruce
Before generating the url just call setHostedDomain against your Google Client
$client->setHostedDomain("HOSTED DOMAIN")
For login with Google using Laravel Socialite
https://laravel.com/docs/8.x/socialite#optional-parameters
use Laravel\Socialite\Facades\Socialite;
return Socialite::driver('google')
->with(['hd' => 'pontomais.com.br'])
->redirect();

Resources