How to use generated accessToken upon Firebase account creation? - firebase

After I create user with email and password as below,
firebase.auth().createUserWithEmailAndPassword(email, password).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// ...
});
firebase returns me created user object like this:
{
"uid": "huuX6OwdbfNvthORiDzNV6seo3D3",
"displayName": null,
"photoURL": null,
"email": "qqq2#yahoo.com",
"emailVerified": false,
"phoneNumber": null,
"isAnonymous": false,
"providerData": [
{
"uid": "qqq2#yahoo.com",
"displayName": null,
"photoURL": null,
"email": "qqq2#yahoo.com",
"phoneNumber": null,
"providerId": "password"
}
],
"apiKey": "qqqwwweee",
"appName": "[DEFAULT]",
"authDomain": null,
"stsTokenManager": {
"apiKey": "qqqwwweee",
"refreshToken": "AGdpqewb4UYbr5Uo5",
"accessToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImI4OWY",
"expirationTime": 1532487232918
},
"redirectEventId": null,
"lastLoginAt": "1532483591000",
"createdAt": "1532482346000"
}
Then every time I use signInWithEmailAndPassword method it generates new accessToken and refreshToken.
But I would like to use accessToken until it expires for created user, what method I can use to validate user with this token. So, I do not need to make user sign-in every time.
And method provided for this?

You are accessing internals of a user. These are subject to change. You should rely on the officially documented API. accessToken is basically the internal name of Firebase ID token. The correct way to get it is by calling user.getIdToken(). This will return the cached token if not expired or will refresh the token under the hood for you.

Related

Identity Server Random "invalid_grant"

Hello I have an issue with IdentityServer 4.
When we intiate a cal to IDP with Swagger for the first time I can get a token
[12:36:21 DBG] Getting claims for identity token for subject: 3680d5aa-4b35-4e39-a1ce-cfbc6961f4c3 and client: Idp.UserIdentitySwagger
[12:36:21 DBG] In addition to an id_token, an access_token was requested. No claims other than sub are included in the id_token. To obtain more user claims, either use the user info endpoint or set AlwaysIncludeUserClaimsInIdToken on the client configuration.
[12:36:21 VRB] Creating JWT identity token
[12:36:21 INF] {"ClientId": "Idp.UserIdentitySwagger", "ClientName": "Idp.UserIdentity Swagger", "RedirectUri": null, "Endpoint": "Token", "SubjectId": "3680d5aa-4b35-4e39-a1ce-cfbc6961f4c3", "Scopes": "openid profile email", "GrantType": "authorization_code", "Tokens": [{"TokenType": "id_token", "TokenValue": "****dlrQ", "$type": "Token"}, {"TokenType": "access_token", "TokenValue": "****g_rw", "$type": "Token"}], "Category": "Token", "Name": "Token Issued Success", "EventType": "Success", "Id": 2000, "Message": null, "ActivityId": "0HMJ7TTLK79RA:0000000E", "TimeStamp": "2022-07-17T12:36:21.0000000Z", "ProcessId": 1, "LocalIpAddress": "10.244.1.16:443", "RemoteIpAddress": "10.244.0.9", "$type": "TokenIssuedSuccessEvent"}
[12:36:21 VRB] Identity token issued for Idp.UserIdentitySwagger (Idp.UserIdentity Swagger) / 3680d5aa-4b35-4e39-a1ce-cfbc6961f4c3: eyJhbGciOiJSUzI1NiIsImtpZCI6IjIzNTJFMjcwQkFDQjUwMDAwNjM1NkY3RjIwRDM0MEIwQjk3NDRCRThSUzI1NiIsInR5cCI6IkpXVCIsIng1dCI6IkkxTGljTHJMVUFBR05XOV9JTk5Bc0xsMFMtZyJ9.eyJuYmYiOjE2NTgwNjEzODEsImV4cCI6MTY1ODA2NDk4MSwiaXNzIjoiaHR0cHM6Ly9pZHAub3Vpb3VpZGlzY291bnQuY29tIiwiYXVkIjoiSWRwLlVzZXJJZGVudGl0eVN3YWdnZXIiLCJpYXQiOjE2NTgwNjEzODEsImF0X2hhc2giOiJFWWRZYWtpb0ZFUTN6Z19qeHZ1Umd3Iiwic19oYXNoIjoiMUxYeTNQMXpaOTZiU2lDWjBrRmNBZyIsInNpZCI6IjYzREQ4OEQ5QTQ0NEEyRDQzRDU1QUNBMjYyQTM1MTc3Iiwic3ViIjoiMzY4MGQ1YWEtNGIzNS00ZTM5LWExY2UtY2ZiYzY5NjFmNGMzIiwiYXV0aF90aW1lIjoxNjU4MDYxMzczLCJpZHAiOiJsb2NhbCIsImFtciI6WyJwd2QiXX0.kyOSEob49JTd10Wmz3YMgg48MB-RRWmSJ6JB2dZeu-0r8WPOK69XXlq74bGAoyV6DwytsyTOmwb7h5Wnu5zcgbHFJ_ycGAi5PwOiO1clyDIpYW5ql__SZ2JH31ppuRg616eDaX0M2p9PFfW4MBSM1d4p69aWrbqAmuj8g833VjtZOFkZcgS6OZotqbM_zxOGLhfkzwJQtDjHdh1_imJp80fa4uv_0KOpWc62hclOXcBS8oKvgQYyeeS8AIXGrIBoNII8ZQ8yK-BrqOAjm4f1PVyyhQa8P19gXWoASQL6EHb-zCUo5VUXAu7bukBb4JNNzk8jUTCWvSUo9z4_rDdlrQ
[12:36:21 VRB] Access token issued for Idp.UserIdentitySwagger (Idp.UserIdentity Swagger) / 3680d5aa-4b35-4e39-a1ce-cfbc6961f4c3: eyJhbGciOiJSUzI1NiIsImtpZCI6IjIzNTJFMjcwQkFDQjUwMDAwNjM1NkY3RjIwRDM0MEIwQjk3NDRCRThSUzI1NiIsInR5cCI6ImF0K2p3dCIsIng1dCI6IkkxTGljTHJMVUFBR05XOV9JTk5Bc0xsMFMtZyJ9.eyJuYmYiOjE2NTgwNjEzODEsImV4cCI6MTY1ODA2NDk4MSwiaXNzIjoiaHR0cHM6Ly9pZHAub3Vpb3VpZGlzY291bnQuY29tIiwiY2xpZW50X2lkIjoiSWRwLlVzZXJJZGVudGl0eVN3YWdnZXIiLCJzdWIiOiIzNjgwZDVhYS00YjM1LTRlMzktYTFjZS1jZmJjNjk2MWY0YzMiLCJhdXRoX3RpbWUiOjE2NTgwNjEzNzMsImlkcCI6ImxvY2FsIiwic2lkIjoiNjNERDg4RDlBNDQ0QTJENDNENTVBQ0EyNjJBMzUxNzciLCJpYXQiOjE2NTgwNjEzODEsInNjb3BlIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJlbWFpbCJdLCJhbXIiOlsicHdkIl19.BXtDlQKqw8rGmgnLjJuWJicF2BIHPzpp48NC-aP9cpzy7dqYY2a8kI1x07vTnhX2rcEjdfqhfMIfyJuqZJBhXVtfI7R60QyfuAj3Ozpa4KGE2Y28d9Xntizf4ctwUXFLZVboH8MrXflcIiDL8s5h_c6P6W2NafYK_1m7xpU68Qq0NsxqXsaG2SZT_nph-bl_hEvfR_AfXbkDI12Z606hSqAhjP5v_TQfc6_0zveCVTiFRUMCzTzndtRSVtNrP3WPGXalOTtOaeOIUFssDvqNYeF6nch245vjw5NQQu3zUgETOSJfeO_d0c7VCeEvp_s_yCEFCVOIl2_xvWd3Hig_rw
I disconnect and try to login again and a “invalid_grant” is raised. I clear the cache and try again and it does not work. After several retries I can login. I think it is random error. I cannot figure out the reason behind this error. Here is the error message and the IDP configuration and the client configuration. I hope that it can help.
[12:42:06 DBG] A data reader was disposed.
[12:42:06 DBG] Closing connection to database 'IdpDb' on server 'tcp://XXXXXXXXXXX:5432'.
[12:42:06 DBG] Closed connection to database 'IdpDb' on server ''.
[12:42:06 DBG] tJc155MKnmvPDXowrLH4laE8GBDyxFtEveiaB/ONE4w= found in database: False
[12:42:06 DBG] authorization_code grant with value: E3661868CE07773D4612B6A32A5D10B9B0A48D00E616718C795D9ED5F6827348 not found in store.
[12:42:06 ERR] Invalid authorization code{"code": "E3661868CE07773D4612B6A32A5D10B9B0A48D00E616718C795D9ED5F6827348"}, details: {"ClientId": "Idp.UserIdentitySwagger", "ClientName": "Idp.UserIdentity Swagger", "GrantType": "authorization_code", "Scopes": null, "AuthorizationCode": "****7348", "RefreshToken": "********", "UserName": null, "AuthenticationContextReferenceClasses": null, "Tenant": null, "IdP": null, "Raw": {"grant_type": "authorization_code", "code": "E3661868CE07773D4612B6A32A5D10B9B0A48D00E616718C795D9ED5F6827348", "client_id": "Idp.UserIdentitySwagger", "client_secret": "***REDACTED***", "redirect_uri": "https://identity.*******.com/swagger/oauth2-redirect.html", "code_verifier": "eMCIRwHDzhTf1YrRr651Uaqi_COopBhc7ZfOGyjRiAc"}, "$type": "TokenRequestValidationLog"}
[12:42:06 INF] {"ClientId": "Idp.UserIdentitySwagger", "ClientName": "Idp.UserIdentity Swagger", "RedirectUri": null, "Endpoint": "Token", "SubjectId": null, "Scopes": null, "GrantType": "authorization_code", "Error": "invalid_grant", "ErrorDescription": null, "Category": "Token", "Name": "Token Issued Failure", "EventType": "Failure", "Id": 2001, "Message": null, "ActivityId": "0HMJ7TTLK79RH:00000008", "TimeStamp": "2022-07-17T12:42:06.0000000Z", "ProcessId": 1, "LocalIpAddress": "10.244.1.16:443", "RemoteIpAddress": "10.244.0.9", "$type": "TokenIssuedFailureEvent"}
[12:42:06 VRB] Invoking result: IdentityServer4.Endpoints.Results.TokenErrorResult
IdpConfiguration
services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
})
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = (t) =>
{
t.UseNpgsql(configuration.GetConnectionString("IdpDb"),
b => b.MigrationsAssembly(migrationsAssembly));
t.EnableSensitiveDataLogging();
};
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = (t) =>
{
t.UseNpgsql(configuration.GetConnectionString("IdpDb"),
b => b.MigrationsAssembly(migrationsAssembly));
t.EnableSensitiveDataLogging();
};
})
.AddProfileService<BrandeeUserProfileService>()
.AddSigningCredential(LoadCertificate(configuration));
DataProtection Code:
services.AddDataProtection()
.SetApplicationName("TAASe")
.UseCryptographicAlgorithms(
new AuthenticatedEncryptorConfiguration()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256,
}
)
.ProtectKeysWithCertificate(new X509Certificate2(configuration["Certificate:Path"],
configuration["Certificate:Password"]))
.PersistKeysToDbContext<AppDataProtectionDbContext>()
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
Client Definition:
{
"clientId": "Idp.UserIdentity Swagger",
"clientName": "Idp.UserIdentity Swagger",
"requireConsent": true,
"accessTokenLifetime": 3600,
"identityTokenLifetime": 3600,
"allowOfflineAccess": true,
"alwaysSendClientClaims": true,
"secrets": [
"secret"
],
"scopes": [
"openid","profile","email"
],
"allowedGrantType": [
"authorization_code"
],
"redirectUris": [
"https://identity.XXXXXXXXXX.com/swagger/oauth2-redirect.html"
],
"corsOrigins": [
"https://identity.XXXXXXX.com"
],
"postLogoutRedirectUri": []
}
and in Swagger
services.AddSwaggerGen(options =>
{
var oauthSecuritySchema = new OpenApiSecurityScheme()
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows()
{
AuthorizationCode = new OpenApiOAuthFlow()
{
AuthorizationUrl = new Uri(configuration["Idp:AuthorizationUrl"]),
Scopes = new Dictionary<string, string>()
{
// {"Idp.UserManagement","Identity"},
{"openid","openid"},
{"profile","profile"},
{"email","email"}
},
TokenUrl = new Uri(configuration["Idp:TokenUrl"]),
}
},
Name = configuration["Swagger:Name"],
};
options.SwaggerDoc("v1", new OpenApiInfo {Title = "Protected API", Version = "v1"});
options.AddSecurityDefinition("oauth2", oauthSecuritySchema);
options.OperationFilter<AuthorizeCheckOperationFilter>();
options.EnableAnnotations();
});
When the user logs in, the Idp saves the code in the PersistedGrants table with the key formed by (code + ":" + "authorization_code").Sha256(). For your code E3661868CE07773D4612B6A32A5D10B9B0A48D00E616718C795D9ED5F6827348 the key is tJc155MKnmvPDXowrLH4laE8GBDyxFtEveiaB/ONE4w=. That is correct according to the logs. Then, the Idp returns this code to the caller.
Then, the client can change this code for the tokens in the token endpoint. The Idp try to recover this record by forming the key again, retrieving it from the database and checking if the passed code match with the stored previously.
It seems the persistence in the operational store is not working properly. Either this record is not saved in the first part, or the query to get it in the second part is not working. Whatever the reason the service returns a generic invalid_grant error message.
Check if the record is saved in the PersistedGrants table. You can use my Fiddle to form the key and test with other codes.
Check the previous logs if you have any EF exception when SaveAsync is called.

How to extract data from response params in the custom identity provider

Since the latest maintenances from ConnectyCube servers, I am experiencing a new issue when I authenticate with the custom identity provider.
I use Firebase as endpoint and it return the following json file (tested with Postman) :
{
"kind": "identitytoolkit#GetAccountInfoResponse",
"users": [
{
"localId": "The uid that I want to get",
"email": "test#test.com",
"passwordHash": "xxxxxx",
"emailVerified": false,
"passwordUpdatedAt": 1607681539305,
"providerUserInfo": [
{
"providerId": "password",
"federatedId": "test#test.com",
"email": "test#test.com",
"rawId": "test#test.com"
}
],
"validSince": "1607681539",
"disabled": false,
"lastLoginAt": "1620422687374",
"createdAt": "1607681539305",
"lastRefreshAt": "2021-05-07T21:24:47.374Z"
}
]
}
Previously, to get the localId, I used the following as responses params {"uid": "#{users[0].localId}"} and it worked.
Now I get the error "base":["Custom Identity Provider uid is required"] when I try to sign in the users while nothing changed in my code.
Do you have a solution to access to the localID parameter without using [0] as it seems to be the cause of the issue?
My current settings for the custom identity provider:
ENDPOINT: https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=API_WEBKEY
REQUEST HEADERS: {"Content-Type": "application/json"}
REQUEST PARAMS: {"idToken": "#{login}"}
RESPONSE PARAMS: {"uid": "#{users[0].localId}"}
Extract of my script where the error appear:
final token = await FirebaseLib().auth.currentUser.getIdToken();
createSession().then((cubeSession) {
CubeUser user = CubeUser(login: "$token", password: "");
signIn(user).then((cubeUser) {
Print("Worked!");
}).catchError((error) {});
}).catchError((error) {});
This should work now again
Please check

Is there any way to get the "emailVerified" from my firebase auth user using ''auth REST API''?

I'm currently using firebase as my backend webserver , and I used the auth REST API to authenticate users in my app (login using email and password ). My question is I want to know if there is an http request that I can a response by it that includes if the email that logged in is verified or not , I have tried mixing the rest API with the firebase-Auth package but it didn't work .
I just found the "setAccountInfo" endpoint, but for it to work I need the " oobCode " from the confirmation email that was send to the user and I can't get that automatically, I guess.
Can anyone help me on this?
I think you can do that with the getAccountInfo endpoint, which returns the following payload (sample from the doc) that contains a emailVerified property:
{
"users": [
{
"localId": "ZY1rJK0...",
"email": "user#example.com",
"emailVerified": false,
"displayName": "John Doe",
"providerUserInfo": [
{
"providerId": "password",
"displayName": "John Doe",
"photoUrl": "http://localhost:8080/img1234567890/photo.png",
"federatedId": "user#example.com",
"email": "user#example.com",
"rawId": "user#example.com",
"screenName": "user#example.com"
}
],
"photoUrl": "https://lh5.googleusercontent.com/.../photo.jpg",
"passwordHash": "...",
"passwordUpdatedAt": 1.484124177E12,
"validSince": "1484124177",
"disabled": false,
"lastLoginAt": "1484628946000",
"createdAt": "1484124142000",
"customAuth": false
}
]
}
Sample Request (from the doc, pasted for reference):
curl
'https://identitytoolkit.googleapis.com/v1/accounts:lookup?key=[API_KEY]'
\
-H 'Content-Type: application/json' --data-binary '{"idToken":"[FIREBASE_ID_TOKEN]"}'
In the example above, you would replace [API_KEY] with the Web API Key
of your Firebase project and [FIREBASE_ID_TOKEN] with the user's
Firebase ID token.
PS: I've not tried the setAccountInfo endpoint you mention at the end of your question, but I think it is used for sending (POST) the verification code received by email (From the doc: -> "oobCode: The action code sent to user's email for email verification.")
import pyrebase
firebaseConfig = {
"apiKey": "",
"authDomain": "",
"projectId": "",
"storageBucket": "",
"messagingSenderId": "",
"appId": "",
"measurementId": "",
"databaseURL": ""
}
firebase = pyrebase.initialize_app(firebaseConfig)
auth = firebase.auth()
email= "your Email"
password = "your Password"
login = auth.sign_in_with_email_and_password(email, password)
acc_info=auth.get_account_info(login['idToken'])
if "users" in acc_info:
if acc_info["users"]:
for val in acc_info['users']:
if "emailVerified" in val:
print(val["emailVerified"])

Firebase.auth error with 400 bad request

I found a strange error while I developing system using Firebase with service url contains user data.
User data is below.
{
"uid": "kt9Hcp2FbYbBvvIeSHHa1RbvHcv2",
"displayName": "Anonymous 901",
"photoURL": null,
"email": null,
"emailVerified": false,
"identifierNumber": null,
"isAnonymous": true,
"providerData": [
],
"apiKey": "MyApiKeyString",
"appName": "MyAppName",
"authDomain": "my.auth.domain",
"stsTokenManager": {
"apiKey": "MyApiKeyString",
"refreshToken": "refreshTokenString",
"accessToken": "accessTokenString",
"expirationTime": 1532451863076
},
"redirectEventId": null
}
I encode the above anonymous user data and include it in the service url.
( http://myserviceurl?userdata=encodedUserData )
Inside the system receives that url, firebase creates a user object with that user data contained in the url.
The purpose of this url is to use specific user's information in any browser.
However, when I call that service url, sometimes system creates user object well, sometimes got error -
400 Bad request errors with
https://www.googleapis.com/identitytoolkit/v3/relyingparty/setAccountInfo?key=MyApiKeyString
And error data is below,
{
"error": {
"code": 400,
"message": "TOKEN_EXPIRED",
"errors": [
{
"message": "TOKEN_EXPIRED",
"domain": "global",
"reason": "invalid"
}
]
}
}
Few hours later it works well, I changed nothing though.
I could not find the exact error point, but I suspect error occurs while observing authentication state or before this step.
Here is code snipets
#bind
private makeUserLoadingPromise(): Promise<void> {
let unSubscribe: () => void;
return new Promise<void>((resolve, _reject) => {
const onInitialized = this.makeOnInitializedAuthStateChanged(resolve);
unSubscribe = this.auth.onAuthStateChanged(onInitialized);
}).then(() => {
unSubscribe();
this.auth.onAuthStateChanged(this.onAuthStateChanged);
});
}
#bind
private makeOnInitializedAuthStateChanged(resolve: () => void) {
return (user: firebase.User | null) => {
this.user = user;
resolve();
};
}
#bind
private onAuthStateChanged(user: firebase.User | null) {
this.user = user;
}
Or maybe it relates with expirationTime?
I couldn't find any hints about this situation.
Any advice would be appreciated.
It is not clear what you are doing, but it appears that you are using the API incorrectly and insecurely. The plain user object contains a refresh token that is indefinite. Passing it around via URL is a really bad idea.
First don't rely on internal implementations, it is subject to change.
To get the user's information on your backend, the right way to do it, is to get the user's ID token using officially supported API, eg user.getIdToken(), then pass it to your server.
On your server, you verify it via the Firebase Admin SDK: admin.auth().verifyIdToken(idToken). Then you know this is a real authenticated user. If you need the full user info, you can then look it up using the decoded user id in the token: admin.auth().getUser(decodedIdToken.sub).

Using $set() to add a User object to /users in Firebase

I'm currently trying to add new users to Firebase via the AngularFire $set() method. I'm creating new users via the $createUser method from AngularFire. My code looks like this:
$scope.createUser = function() {
$scope.auth.$createUser('dolly#gmail.com', 'password').then(function(user, err) {
if (err) {
console.log('ERROR: ' + err);
} else {
sync.$set('users', user);
}
});
}
This is a creating new user and placing the new user object inside of users:{..}, however, it is also adding an additional user child object thats just duplicate data -- this is what the code is adding to Firebase:
{
"users": {
"email": "dolly#gmail.com",
"id": "11",
"isTemporaryPassword": false,
"md5_hash": "xxxxxxxxxxxxxxx",
"provider": "password",
"sessionKey": "xxxxxxxxxxxxx",
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0MDc5NDQ2NDYsInYiOjAsImQiOnsicHJvdmlkZXIiOiJwYXNzd29yZCIsImlkIjoiMTEiLCJ1aWQiOiJzaW1wbGVsb2dpbjoxMSIsImVtYWlsIjoiZG9sbHlAZ21haWwuY29tIiwibWQ1X2hhc2giOiIzsdrggfeedsaadrfcDc0ZDRhMTU5NTk2NzI1NzFmMDk2ZTZlNyIsImlzVGVtcG9yYXJ5UGFzc3dvcmQiOmZhbHNlLCJzZXNzaW9uS2V5IjoiM2MwMDNjODkxMDEzOWE5MjhlZTZjNWI1NjU5ZTRiZjMifSwiaWF0IjoxNDA3ODU4MjQ2fQ.p7-9GDtaNpBn1ICTLIUSwlPytaUGi-jyBgcO-LKHUys",
"uid": "simplelogin:11",
"user": {
"email": "dolly#gmail.com",
"id": "11",
"isTemporaryPassword": false,
"md5_hash": "xxxxxxxxxxxxxxx",
"provider": "dfrggrssxxxxxxx",
"sessionKey": "xxxxxxxxxxxxxxx",
"uid": "simplelogin:11"
}
}
I ideally want my users object to look like the example in firebase, except with each user key to be whatever is inside user.uid
users: {
user1: {
name: "Alice"
},
user2: {
name: "Bob"
}
}
Where each new user will be added to the users: {...} key without the duplicate user child tacked on?
If you create a reference to users/ and then call $set on that path, then whatever data you include will replace anything at that path. This is AngularFire 101. You should begin by reading at least that section if not the entire guide.
Choose the path you want to set data at when creating your sync object.
var ref = new Firebase('.../users/'+user.uid);
var sync = $firebase(ref);
sync.$set({ email: user.email, provider: user.provider });
Or, better yet, just set it on the reference since you aren't utilizing this for client-side bindings.
var ref = new Firebase('.../users/'+user.uid);
ref.set({ email: user.email, provider: user.provider });
Creating profiles is explained in the Firebase docs and covered in the AngularFire-Seed repo's createProfile service, nearly verbatim to your example.

Resources