Is Single Sender Validation in Sendgrid possible without logging in? - validation

Was just wondering if it's possible for Single Sender Validation to be completed without having to login to Sendgrid as part of the process (e.g. click-through without login). For context, sometimes the people who "own" a mail address that we want to use for sending don't have access to Sendgrid, and we'd like them to be able to validate it. I think they can't by design, but wanted to confirm.
Looking at the API documentation, it looks like you can use the token sent in the validation email to complete the validation process, but I'm not sure if there's any way to effectively make use of that to redirect the user back to a process we control. There's another post that mentions the same kind of challenge, but thought I'd ask again as there wasn't anything definitive.
Is there a simple way to have the user who receives the validation redirect back to something other than sendgrid directly?
Thanks in advance!

The only alternative to logging in is to use the SendGrid API.
First, you either request the verification using the UI, or you use the Create Verified Sender Request API to start the verification for the single sender.
Then, the verification email will be sent to the specified email address which contains the verification URL. Usually, this URL will redirect you the the actual URL containing the verification token, as mentioned in the SO post you linked.
Once you get the verification token, you can use the Verify Sender Request API, passing in the verification token, to verify the single sender.
Note: All these APIs require a SendGrid API key.
So technically, you could have an application that prompts your user for their email address to verify, then uses the SendGrid API to start the verification which sends the verification email, then ask the user to go to their email inbox and copy in their verification link, then let the user paste in the URL from which you can extract the verification token, and use the API to verify. While the user didn't have to log in, it still requires some manual work.
However, the inputting of the email address and the checking the email inbox can also be done programmatically, so this process can be 100% automated, although it takes quite a bit of programming.
Here's a C# sample:
using System.Net;
using Microsoft.AspNetCore.WebUtilities;
using SendGrid;
namespace VerifySender;
internal class Program
{
public static async Task Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddUserSecrets<Program>(optional: true)
.Build();
var apiKey = configuration["SendGrid:ApiKey"]
?? Environment.GetEnvironmentVariable("SENDGRID_API_KEY")
?? throw new Exception("SendGrid API Key not configured.");
var client = new SendGridClient(apiKey);
// replace this JSON with your own values
const string data = """
{
"nickname": "Orders",
"from_email": "orders#example.com",
"from_name": "Example Orders",
"reply_to": "orders#example.com",
"reply_to_name": "Example Orders",
"address": "1234 Fake St",
"address2": "PO Box 1234",
"state": "CA",
"city": "San Francisco",
"country": "USA",
"zip": "94105"
}
""";
var response = await client.RequestAsync(
method: SendGridClient.Method.POST,
urlPath: "verified_senders",
requestBody: data
);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"Failed to request sender verification. HTTP status code {response.StatusCode}.");
Console.WriteLine(await response.Body.ReadAsStringAsync());
Console.WriteLine(response.Headers.ToString());
}
Console.WriteLine("Enter verification URL:");
var verificationUrl = Console.ReadLine();
var token = await GetVerificationTokenFromUrl(verificationUrl);
response = await client.RequestAsync(
method: SendGridClient.Method.GET,
urlPath: $"verified_senders/verify/{token}"
);
if (!response.IsSuccessStatusCode)
{
Console.WriteLine($"Failed to verify sender. HTTP status code {response.StatusCode}.");
Console.WriteLine(await response.Body.ReadAsStringAsync());
Console.WriteLine(response.Headers.ToString());
}
}
private static async Task<string> GetVerificationTokenFromUrl(string url)
{
/*
* url could be three different types:
* 1. Click Tracking Link which responds with HTTP Found and Location header to url type 2.
* 2. URL containing the verification token:
* https://app.sendgrid.com/settings/sender_auth/senders/verify?token=[VERIFICATION_TOKEN]&etc=etc
* 3. URL prompting the user to login, but contains url 2. in the redirect_to parameter:
* https://app.sendgrid.com/login?redirect_to=[URL_TYPE_2_ENCODED]
*/
const string verificationBaseUrl = "https://app.sendgrid.com/settings/sender_auth/senders/verify";
const string loginBaseUrl = "https://app.sendgrid.com/login";
if (url.StartsWith(verificationBaseUrl))
{
var uri = new Uri(url, UriKind.Absolute);
var parameters = QueryHelpers.ParseQuery(uri.Query);
if (parameters.ContainsKey("token"))
{
return parameters["token"].ToString();
}
throw new Exception("Did not find token in verification URL.");
}
if (url.StartsWith(loginBaseUrl))
{
var uri = new Uri(url, UriKind.Absolute);
var parameters = QueryHelpers.ParseQuery(uri.Query);
if (parameters.ContainsKey("redirect_to"))
{
url = $"https://app.sendgrid.com{parameters["redirect_to"]}";
return await GetVerificationTokenFromUrl(url);
}
throw new Exception("Did not find token in verification URL.");
}
var clientHandler = new HttpClientHandler();
clientHandler.AllowAutoRedirect = false;
using var httpClient = new HttpClient(clientHandler);
var response = await httpClient.GetAsync(url);
if (response.StatusCode == HttpStatusCode.Found)
{
var uri = response.Headers.Location;
return await GetVerificationTokenFromUrl(uri.ToString());
}
throw new Exception("Did not find token in verification URL.");
}
}
Take note of the comments inside of GetVerificationTokenFromUrl. Since I don't trust the user to copy the URL from the email without clicking on it, I added support for three types of URL:
Click Tracking Link which responds with HTTP Found and Location header to url type 2.
URL containing the verification token: https://app.sendgrid.com/settings/sender_auth/senders/verify?token=[VERIFICATION_TOKEN]&etc=etc
URL prompting the user to login, but contains url 2. in the redirect_to parameter: https://app.sendgrid.com/login?redirect_to=[URL_TYPE_2_ENCODED]
Here's the full source code on GitHub.

Related

Calling rest server from mobile app

Following on from https://lists.hyperledger.org/g/composer/message/91
I have adapted the methodology described by Caroline Church in my IOS app.
Again I can authenticate with google but still get a 401 authorization error when POSTing.
I have added the withCredentials parameter to the http header in my POST request.
does the rest server pass back the token in cookie ? I don't receive anything back from the rest server.
where does the withCredentials get the credentials from ?
COMPOSER_PROVIDERS as follows
COMPOSER_PROVIDERS='{
"google": {
"provider": "google",
"module": "passport-google-oauth2",
"clientID": "93505970627.apps.googleusercontent.com",
"clientSecret": "",
"authPath": "/auth/google",
"callbackURL": "/auth/google/callback",
"scope": "https://www.googleapis.com/auth/plus.login",
"successRedirect": "myAuth://",
"failureRedirect": "/"
}
}'
the successRedirect points back to my App. After successfully authenticating I return to the App.
Got this working now. The App first authenticates with google then exchanges the authorization code with the rest server.
The Rest server COMPOSER_PROVIDERS needs to be changed to relate back to the app.
clientID is the apps ID in google,
callbackURL and successRedirect are reversed_clientID://
The App calls http://localhost:3000/auth/google/callback with the authorization code as a parameter.
this call will fail, but an access_token cookie is written back containing the access token required for the rest server.
The user id of the logged in user is not passed back, when exchanging the code for a token with google we get back a JWT with the details of the logged in user. We need this back from the rest server as well as the token. Is there any way to get this ?
changing the COMPOSER_PROVIDERS means that the explorer interface to the Rest server no longer works.
func getRestToken(code: String) {
let tokenURL = "http://localhost:3000/auth/google/callback?code=" + code
let url = URL(string:tokenURL);
var request = URLRequest(url: url!);
request.httpMethod = "GET";
request.setValue("localhost:3000", forHTTPHeaderField: "Host");
request.setValue("text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8", forHTTPHeaderField: "Accept");
request.setValue("1", forHTTPHeaderField: "Upgrade-Insecure-Requests");
request.httpShouldHandleCookies = true;
request.httpShouldUsePipelining = true;
let session = URLSession.init(configuration: .default);
session.configuration.httpCookieAcceptPolicy = .always;
session.configuration.httpShouldSetCookies=true;
session.configuration.httpCookieStorage = HTTPCookieStorage.shared;
let task = session.dataTask(with: request) { (data, response, error) in
var authCookie: HTTPCookie? = nil;
let sharedCookieStorage = HTTPCookieStorage.shared.cookies;
// test for access_token
for cookie in sharedCookieStorage! {
if cookie.name == "access_token"
{
print(“Received access token”)
}
}
guard error == nil else {
print("HTTP request failed \(error?.localizedDescription ?? "ERROR")")
return
}
guard let response = response as? HTTPURLResponse else {
print("Non-HTTP response")
return
}
guard let data = data else {
print("HTTP response data is empty")
return
}
if response.statusCode != 200 {
// server replied with an error
let responseText: String? = String(data: data, encoding: String.Encoding.utf8)
if response.statusCode == 401 {
// "401 Unauthorized" generally indicates there is an issue with the authorization
print("Error 401");
} else {
print("HTTP: \(response.statusCode), Response: \(responseText ?? "RESPONSE_TEXT")")
}
return
}
}
task.resume()
}
have you authorised the redirect URI in your Google OAUTH2 configuration ?
This determines where the API server redirects the user, after the user completes the authorization flow. The value must exactly match one of the redirect_uri values listed for your project in the API Console. Note that the http or https scheme, case, and trailing slash ('/') must all match.
This is an example of an Angular 5 successfully using it Angular 5, httpclient ignores set cookie in post in particular the answer at the bottom
Scope controls the set of resources and operations that an access token permits. During the access-token request, your application sends one or more values in the scope parameter.
see https://developers.google.com/identity/protocols/OAuth2
The withCredentials option is set, in order to create a cookie, to pass the authentication token, to the REST server.
Finally this resource may help you https://hackernoon.com/adding-oauth2-to-mobile-android-and-ios-clients-using-the-appauth-sdk-f8562f90ecff

How to know that access token has expired?

How should client know that access token has expired, so that he makes a request with refresh token for another access token?
If answer is that server API will return 401, then how can API know that access token has expired?
I'm using IdentityServer4.
Your api should reject any call if the containing bearer token has already been expired. For a webapi app, IdentityServerAuthenticationOptions will do the work.
But your caller Web application is responsible for keeping your access_token alive. For example, if your web application is an ASP.Net core application, you may use AspNetCore.Authentication.Cookies to authenticate any request. In that case, you can find the information about the token expiring info through OnValidatePrincipal event.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationScheme = "Cookies",
//ExpireTimeSpan = TimeSpan.FromSeconds(100),
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Events = new CookieAuthenticationEvents()
{
OnValidatePrincipal = async x =>
{
if (x.Properties?.Items[".Token.expires_at"] == null) return;
var now = DateTimeOffset.UtcNow;
var tokenExpireTime = DateTime.Parse(x.Properties.Items[".Token.expires_at"]).ToUniversalTime();
var timeElapsed = now.Subtract(x.Properties.IssuedUtc.Value);
var timeRemaining = tokenExpireTime.Subtract(now.DateTime);
if (timeElapsed > timeRemaining)
{
//Get the new token Refresh the token
}
}
}
}
I have added a full implementation about how to get a new access token using refresh token in another StackOverflow answer

google ExchangeCodeForTokenAsync invalid_grant in webapi

i have implemented GoogleAuthorizationCodeFlow scenario from google api client dotnet and tutorial to get token from what my client sent to server as a code. but when i call flow.ExchangeCodeForTokenAsync , I get the following error :
{"Error:\"invalid_grant\", Description:\"\", Uri:\"\""}
I read google authorization invalid_grant and gusclass oauth 2 using google dotnet api client libraries but they didn't help me and. I think it must be very simple but I don't know why it doesn't work.
For client side , I have used Satellizer and this is my server Codes:
public bool PostExchangeAccessToken(GoogleClientAccessCode code)
{
string[] SCOPES = { "email" };
IAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets()
{
ClientSecret = "******",
ClientId = "********.apps.googleusercontent.com"
},
Scopes = SCOPES
});
try
{
TokenResponse token;
token = flow.ExchangeCodeForTokenAsync("*****#gmail.com", Newtonsoft.Json.JsonConvert.SerializeObject(code), "https://localhost:44301/",
CancellationToken.None).Result;
}
catch (Exception ex)
{
throw ex;
}
return true;
}
what is the problem?
On Github I found that I must use the Token from the client and use
GoogleAuthorizationCodeFlow.Initializer()
to create my UserCredential object.
You can check your google developer console settings.(Authorized redirect URIs)
Credentials => OAuth 2.0 client IDs => Your Application Settings => Authorized redirect URIs
You must add url. ("https://localhost:44301/")
My code :
flow.ExchangeCodeForTokenAsync("me", authCode, redirectUri, CancellationToken.None).Result;
Authorized redirect URIs
For use with requests from a web server. This is the path in your application that users are redirected to after they have authenticated with Google. The path will be appended with the authorization code for access. Must have a protocol. Cannot contain URL fragments or relative paths. Cannot be a public IP address.

Separating Auth and Resource Servers with AspNet.Security.OpenIdConnect - the Audience?

The example on the AspNet.Security.OpenIdConnect.Server looks to me like both an auth and resource server. I would like to separate those. I have done so.
At the auth server's Startup.Config, I have the following settings:
app.UseOpenIdConnectServer(options => {
options.AllowInsecureHttp = true;
options.ApplicationCanDisplayErrors = true;
options.AuthenticationScheme = OpenIdConnectDefaults.AuthenticationScheme;
options.Issuer = new System.Uri("http://localhost:61854"); // This auth server
options.Provider = new AuthorizationProvider();
options.TokenEndpointPath = new PathString("/token");
options.UseCertificate(new X509Certificate2(env.ApplicationBasePath + "\\mycertificate.pfx","mycertificate"));
});
I have an AuthorizationProvider written, but I don't think it's relevant to my current issue (but possibly relevant). At its GrantResourceOwnerCredentials override, I hard-code a claims principal so that it validates for every token request:
public override Task GrantResourceOwnerCredentials(GrantResourceOwnerCredentialsNotification context)
{
var identity = new ClaimsIdentity(OpenIdConnectDefaults.AuthenticationScheme);
identity.AddClaim(ClaimTypes.Name, "me");
identity.AddClaim(ClaimTypes.Email, "me#gmail.com");
var claimsPrincipal = new ClaimsPrincipal(identity);
context.Validated(claimsPrincipal);
return Task.FromResult<object>(null);
}
At the resource server, I have the following in its Startup.config:
app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), branch =>
{
branch.UseOAuthBearerAuthentication(options => {
options.Audience = "http://localhost:54408"; // This resource server, I believe.
options.Authority = "http://localhost:61854"; // The auth server
options.AutomaticAuthentication = true;
});
});
On Fiddler, I ask for a token, and I get one:
POST /token HTTP/1.1
Host: localhost:61854
Content-Type: application/x-www-form-urlencoded
username=admin&password=aaa000&grant_type=password
So now I use that access token to access a protected resource from the resource server:
GET /api/values HTTP/1.1
Host: localhost:54408
Content-Type: application/json;charset=utf-8
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI.....
I now get this error - Audience validation failed. Audiences: 'empty'. Did not match validationParameters.ValidAudience: 'http://localhost:54408' or validationParameters.ValidAudiences: 'null'.
I think the reason why is because I never set an audience at the auth server (at app.UseOpenIdConnectServer(...)), so I don't think it wrote audience info to the token. So I need to set an audience at the auth server (as what is done in IdentityServer3), but I can't find a property on the options object that would let me do that.
Does AspNet.Security.OpenIdConnect.Server require the auth and resource to be in the same server?
Is setting the audience done when putting together the ClaimsPrincipal, and if so, how?
Would I need to write a custom Audience validator and hook it up to the system? (I sure hope the answer to this is no.)
Does AspNet.Security.OpenIdConnect.Server require the auth and resource to be in the same server?
No, you can of course separate the two roles.
As you've already figured out, if you don't explicitly specify it, the authorization server has no way to determine the destination/audience of an access token, which is issued without the aud claim required by default by the OAuth2 bearer middleware.
Solving this issue is easy: just call ticket.SetResources(resources) when creating the authentication ticket and the authorization server will know exactly which value(s) (i.e resource servers/API) it should add in the aud claim(s).
app.UseOpenIdConnectServer(options =>
{
// Force the OpenID Connect server middleware to use JWT tokens
// instead of the default opaque/encrypted token format used by default.
options.AccessTokenHandler = new JwtSecurityTokenHandler();
});
public override Task HandleTokenRequest(HandleTokenRequestContext context)
{
if (context.Request.IsPasswordGrantType())
{
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "unique identifier");
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Call SetResources with the list of resource servers
// the access token should be issued for.
ticket.SetResources("resource_server_1");
// Call SetScopes with the list of scopes you want to grant.
ticket.SetScopes("profile", "offline_access");
context.Validate(ticket);
}
return Task.FromResult(0);
}
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Audience = "resource_server_1",
Authority = "http://localhost:61854"
});

403 Forbidden Request Using G+ API with good Access Token [duplicate]

I am developing a small web application wherein I am integrating with Google+ Domain API's.
I am using OAuth2 authentication.I have generated client_id and client_secret for my web application
from Google API console.
Using Google+ Domain API's, I am able to generate the access token.
Generating authorization URL
List<String> SCOPE = Arrays.asList(
"https://www.googleapis.com/auth/plus.me",
"https://www.googleapis.com/auth/plus.circles.read",
"https://www.googleapis.com/auth/plus.stream.write");
//Sets up Authorization COde flow
GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(new NetHttpTransport(),
new JacksonFactory(),
"xxx","yyy",SCOPE).setApprovalPrompt("force").setAccessType("offline").build();
//Builds the uthorization URL
String url = flow.newAuthorizationUrl().setRedirectUri(<REDIRECT_URI>).build();
out.println("<div id='googleplus'></div><a href='"+url+"' rel='external' ><img src='googleplus.jpg'></a> <b>Configure</b></div>");
session.setAttribute("CodeFlow", flow);
After authorization
GoogleAuthorizationCodeFlow flow=(GoogleAuthorizationCodeFlow)session. getAttribute("CodeFlow");
//After authorization,fetches the value of code parameter
String authorizationCode=request.getParameter("code");
//Exchanges the authorization code to get the access token
GoogleTokenResponse tokenResponse=flow.newTokenRequest(authorizationCode).
setRedirectUri(<REDIRECT_URI>).execute();
GoogleCredential credential = new GoogleCredential.Builder().setTransport(new NetHttpTransport()).setJsonFactory(new JacksonFactory())
.setClientSecrets("xxx", "yyy")
.addRefreshListener(new CredentialRefreshListener(){
public void onTokenErrorResponse(Credential credential, TokenErrorResponse errorResponse) throws java.io.IOException{
System.out.println("Credential was not refreshed successfully. "
+ "Redirect to error page or login screen.");
}
#Override
public void onTokenResponse(Credential credential, TokenResponse tokenResponse)
throws IOException {
System.out.println("Credential was refreshed successfully.");
System.out.println("Refresh Token :"+tokenResponse.getRefreshToken());
}
}).build();
//Set authorized credentials.
credential.setFromTokenResponse(tokenResponse);
credential.refreshToken();
Fetching circle information:
PlusDomains plusDomains = new PlusDomains.Builder(
new NetHttpTransport(), new JacksonFactory(), credential)
.setApplicationName("DomainWebApp")
.setRootUrl("https://www.googleapis.com/")
.build();
PlusDomains.Circles.List listCircles=plusDomains.circles().list("me");
listCircles.setMaxResults(5L);
System.out.println("Circle URL:"+listCircles.buildHttpRequestUrl());
CircleFeed circleFeed=listCircles.execute();
System.out.println("Circle feed:"+circleFeed);
List<Circle> circles =circleFeed.getItems();
while (circles != null) {
for (Circle circle : circles) {
out.println("Circle name : "+circle.getDisplayName()+" Circle id : "+circle.getId());
}
// When the next page token is null, there are no additional pages of
// results. If this is the case, break.
if (circleFeed.getNextPageToken() != null) {
// Prepare the next page of results
listCircles.setPageToken(circleFeed.getNextPageToken());
// Execute and process the next page request
circleFeed = listCircles.execute();
circles = circleFeed.getItems();
} else {
circles = null;
}
}
I get the below error:
com.google.api.client.googleapis.json.GoogleJsonResponseException: 403 Forbidden
{
"code" : 403,
"errors" : [ {
"domain" : "global",
"message" : "Forbidden",
"reason" : "forbidden"
} ],
"message" : "Forbidden"
}
com.google.api.client.googleapis.json.GoogleJsonResponseException.from(GoogleJsonResponseException.java:145)
com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest.newExceptionOnError(AbstractGoogleJsonClientRequest.java:113)
Note: I have also enabled Google+ Domain API in my Google API Console.
REDIRECT_URI ="http://localhost:8080/DomainWebApp/oauth2callback" since it's a web app.
Any Suggestions?
The first thing to check is that the application is making the call on behalf of a Google Apps user. If the user account is, for example, an #gmail account, the request will not be allowed. The Google+ Domains API only works for Google Apps domain users, and only for requests within their domain.

Resources