where "x" property is used for public key and "d" is for private key. Private key (d) was used for signing. For verification we need to use public key (x). For token validation we will use JsonWebTokenHandler.ValidateTokenAsync() method from Microsoft.IdentityModel.JsonWebTokens. Here is the code which decodes token:
string token = ...;
var jwk = ...; // get EdDSA keys pair
var pubKey = new EdDsaSecurityKey(new Ed25519PublicKeyParameters(Base64UrlEncoder.DecodeBytes(jwk.X), 0));
pubKey.KeyId = jwk.KeyId;
var result = await new JsonWebTokenHandler().ValidateTokenAsync(token, new TokenValidationParameters()
{
ValidIssuer = JwtHelper.GetServiceName(jwk),
AudienceValidator = (AudienceValidator) ((audiences, securityToken, validationParameters) => true), // or whatever logic is needed for verifying aud claimm
IssuerSigningKey = (SecurityKey) pubKey
});
if (!result.IsValid)
throw result.Exception;
json = JWT.Payload(token);
Here we use EdDsaSecurityKey class from ScottBrady.IdentityModel.Tokens. If public key matches private key which was used for signing then result.IsValid will be true (otherwise code will throw exception). At the end we call JWT.Payload() from jose-jwt to get JSON token representation (from which we may get needed claims and other data).
With these techniques you may generate EdDSA keys, sign tokens and verify them. Hopefully information in these posts will help you.
In my previous post of this series I showed how to generate key pair for EdDSA encryption algorithm. Let's now go further and use these keys to sign JWT token. If you remember from previous post "d" property of json object with keys pair belongs to private key. We will use this private key for signing our JWT token.
For creating JWT token we need to define claims. They are app/domain specific. We can add e.g. iss (issuer), exp (expired) and other standard claims (standard claims are defined in RFC 7519). Also we may add custom claims as we need in the app:
List<Claim> claims = ...; // fill claims
Then we need to load private key (from some secrets storage/vault usually):
var jwk = ...; // load private key
this jwk object may be json object showed in my previous post (plus it should have keyId string property for key identifier which may contain e.g. some guid).
var edDsaSecurityKey = new EdDsaSecurityKey(new Ed25519PrivateKeyParameters(Base64UrlEncoder.DecodeBytes(jwk.d), 0));
edDsaSecurityKey.KeyId = jwk.keyId;
var securityTokenHandler = new JwtSecurityTokenHandler();
string token = securityTokenHandler.WriteToken(securityTokenHandler.CreateToken(new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Issuer = ..., // define issuer (iss) claim as you need
Expires = new DateTime(DateTime.UtcNow.AddMinutes(1)), // add expired date as you need
SigningCredentials = new SigningCredentials(edDsaSecurityKey, "EdDSA")
}));
This code will create JWT token signed with EdDSA private key. In the next post I will show how to verify this token using public EdDSA key.
EdDSA is one of the commonly used encryption algorithms atm. There is a trend to use it instead of older RSA alg. In .Net there are not that many resources about it. E.g. popular nuget package jose-jwt (Javascript Object Signing and Encryption) still doesn't support EdDSA. Fortunately there are another packages which support it:
but many basic examples are still missing. In this post I will show how to generate EdDSA key pair in .NET6 using above packages and save it in json format for later use.
Here is the code which generates key pair (before to run it install both nuget packages):
var keyPairGenerator = new Ed25519KeyPairGenerator();
keyPairGenerator.Init(new Ed25519KeyGenerationParameters(new SecureRandom()));
var keyPairParams = keyPairGenerator.GenerateKeyPair();
var privateKeyParams = (Ed25519PrivateKeyParameters)keyPairParams.Private;
var publicKeyParams = (Ed25519PublicKeyParameters)keyPairParams.Public;
var keyPair = new EddsaKeyPair { d = Base64UrlEncoder.Encode(privateKeyParams.GetEncoded()), x = Base64UrlEncoder.Encode(publicKeyParams.GetEncoded()) };
File.WriteAllText("keys.json", JsonConvert.SerializeObject(keyPair));
public class EddsaKeyPair
{
public string kty => "OKP";
public string alg => "EdDSA";
public string crv => "Ed25519";
public string x { get; set; } // public key
public string d { get; set; } // private key
}
As result it will save EdDSA keys pair to keys.json file which will look like this:
If you use private nuget packages source with authentication and Docker in your project you may need to restore packages from this custom packages source within Docker file. In this post I will show how to use Github secrets for that when you build Docker image via docker/build-push-action Github action.
First of all in the yaml file of our Github action we need to pass necessary secrets references to the build action using the following syntax:
After that in Docker file we fetch passed secrets (they are stored to special files under /run/secrets/... path which is available during Docker image build) and will store them to environment variables using export command. After that we will add our private packages source with username and password (using dotnet nuget add source). When it will be done we will be able to run "dotnet restore" command which will restore project dependencies including those which come from private nuget source:
Note that it is important to pipe commands which export environment variables and then use them to the same single RUN command. If you will try to use these variables in separate RUN command "nuget add source" will tell that "Package source with Name: ... added successfully" but then you will get confusing error when will try to run "dotnet restore":
Error NU1301: Unable to load the service index for source
But if everything is done in the way how it is described above then your project dependencies should be restored successfully for your Docker image.
Basic authentication is probably simplest authentication type for Web API when HTTP requests are authenticated using username and passwords provided in HTTP request headers. In this post I will describe how to add basic authentication to ASP.Net Core Web API.
At first we need to add reference to idunno.Authentication.Basic nuget package. It contains infrastructure for basic authentication ready to be used in ASP.Net Core/.NET Core projects. Also we will need simple validation service which will check provided username/password and based on that will authenticate/reject requests:
In our example BasicAuthValidationService simply compares credentials coming from HTTP request with predefined allowed username/password.
Then in Web API's Program.cs we need to configure basic authentication itself. Since username and password are sent in HTTP request headers it is important to force using HTTPS for securing communication with our API. We will do that by adding RequireHttpsAttribute filter and UseHttpsRedirection middleware:
builder.Services.AddScoped<IBasicAuthValidationService>(c =>
{
// get allowed username and password to authenticate requests
string username = ...;
string password = ...;
return new BasicAuthValidationService(uername, password);
});
// add basic auth
builder.Services.AddAuthentication(BasicAuthenticationDefaults.AuthenticationScheme)
.AddBasic(options =>
{
options.Realm = "Test.Api";
options.Events = new BasicAuthenticationEvents
{
OnValidateCredentials = context =>
{
var validationService = context.HttpContext.RequestServices.GetService<IBasicAuthValidationService>();
if (validationService.AreCredentialsValid(context.Username, context.Password))
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.Username, ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(ClaimTypes.Name, context.Username, ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
return Task.CompletedTask;
}
};
});
builder.Services.Configure<MvcOptions>(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
});
var app = builder.Build(); ...
app.UseHttpsRedirection();
Suppose that we develop web API (e.g. https://api.example.com) and want to use self-signed certificate for it's domain name. Since it is web API we usually don't worry a lot about errors or warning which browser may show when you try to access it by url there. At the same time we don't want to compromise security and thus want to allow only our SSL certificate with known hash/thumbprint.
First of all we need to create self-signed certificate for domain name which will be used for web API (it should contain CN=api.example.com otherwise it won't be possible to bind it to custom domain name in Azure App service). Also certificate should be created with exportable private key (pfx) so we can use it in Azure. It can be done by the following PowerShell:
Here we first create certificate itself and then export it's private key to file system.
Next step is to add custom domain name api.example.com to Azure app service where we will host our API. First of all we need to ensure that current pricing tier supports "Custom domains / SSL" feature. Currently minimal pricing tier with this option is B1 (it is not free):
Then we go to App service > Custom domains > Add custom domain and specify desired domain name for our web API:
Before Azure will allow to use custom domain name we will need to prove hostname ownership by adding TXT and CNAME DNS records for specified domain name - it is done in hosting provider control panel (here are detailed instructions of the whole process: Map an existing custom DNS name to Azure App Service).
Last part is related with client which call our web API. Since web API uses self-signed certificate attempt to call it from C# using HttpClient will fail. We need to configure it to allow usage of our SSL certificate but at the same time don't allow to use other self-signed SSL certificates. It can be done with the following C# code:
In this code we instruct ServicePointManager to allow only our self-signed certificate (if error occurred during checking of SSL certificate when connection to remote host is established). Those it won't allow connections to hosts which use other self-signed SSL certificates.
As you probably know in Sql Server we may set limit on db file size (and on db transaction log file size) using the following commands (in example below we limit both files sizes to 100Mb):
ALTER DATABASE {db}
MODIFY FILE (NAME = {filename}, MAXSIZE = 100MB);
GO
ALTER DATABASE {db}
MODIFY FILE (NAME = [{filename}.Log], MAXSIZE = 100MB);
GO
As result if we will check Database properties > Files - we will see these limits on both files:
In order to get these limits programmatically using db_reader permissions we should use another system stored procedure sp_helpdb and provider database name as parameter:
exec sp_helpdb N'{databaseName}'
This stored procedure returns 2 result sets. In 2nd result set it returns field maxsize which returns max size limit for db file. Here is the code which reads maxsize field from result of sp_helpdb proc:
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
using (var cmd = connection.CreateCommand())
{
cmd.CommandText = $"exec sp_helpdb N'{connection.Database}'";
using (var reader = cmd.ExecuteReader())
{
if (reader.NextResult())
{
if (reader.Read())
{
return reader["maxsize"] as string;
}
}
}
}
}
For database used in example above it will return string "100 Mb". Hope that it will help someone.
If you develop Azure function you probably often run them locally on dev
PC rather than in Azure. It simplifies debugging and development. In
this post I will show how to test certificate-based authentication for
Sharepoint Online in Azure functions running locally.
First of all we need to register AAD app in Azure portal and grant it
Sharepoint permissions:
Don't forget to grant Admin consent after adding permissions.
Go to registered AAD app > Certificates & secrets > Certificates > Upload certificate and upload generated .cer file. After upload copy certificate thumbprint - it will be needed for Azure functions below.
In Azure function certificate-based authentication for Sharepoint Online can be done by the following code (using OfficeDevPnP.Core):
using (var authMngr = new OfficeDevPnP.Core.AuthenticationManager())
{
using (var ctx = authMngr.GetAzureADAppOnlyAuthenticatedContext(siteUrl, clientId, tenant, StoreName.My, StoreLocation.CurrentUser, certificateThumbprint))
{
...
}
}
Here we specified clientId of our AAD app, copied certificate thumbprint and tenant in the form {tenant}.onmicrosoft.com.
Before to run it we need to perform one extra step: install certificate to local PC certificates store. It can be done by double click on .pfx file. After that Windows will open Certificate import wizard:
Since our code is using Personal store use Store Location = Current User. Then specify password and import your certificate to the store. You may check that certificate is installed properly by opening MMC console > Add/Remove snapin > Certificates. Imported certificate should appear under Personal > Certificates:
After that you will be able to run Azure functions locally which communicate with Sharepoint Online using certificate-based authentication.
There are several possible ways to calculate database size (the same size which is shown when you right click on the database in Sql Server Management Studio > Properties > Files):
In this example we have 23 Mb of database file and 11 Mb of transaction log, total 34 Mb.
One way we can try is to run the following query:
select db_name(database_id) as database_name,
type_desc,
name,
size/128.0 as CurrentSizeMB
from sys.master_files
WHERE DB_NAME(database_id) = 'MyDatabase'
If you have permissions to run this command result will look like this:
As you can see it shows the same numbers as properties window shown above. The problem is that according to documentation you should have quite high server-level permissions to access sys.master_files:
The minimum permissions that are required to see the corresponding row are CREATE DATABASE, ALTER ANY DATABASE, or VIEW ANY DEFINITION.
If you don’t have one of these permissions result will be empty.
Is it possible to calculate database size having less permissions? E.g. having only db_reader permissions on target database. The answer is yes it is possible. In order to do that we need to use system stored procedure sp_spaceused:
use MyDatabaseName
exec sp_spaceused
It works also with db_reader permissions on the target database. Result will look like this:
It returns 2 result sets and in 1st result set it returns overall database size which is sum of db file name and transaction log (34 Mb in our example).
Sharepoint contains number of OTB web services which are located inside _vti_bin virtual directory. In this article we will check one of them: authentication.asmx. This is quite interesting web service which allows to authenticate your app in Sharepoint FBA site. I.e. allows to send username and password and get FedAuth authentication cookies if provided credentials are valid (the same cookies which are used by Sharepoint FBA site when you successfully logged in via it’s login page).
Note that you may use this web service in server-side apps or mobile apps but not in client side JavaScript-based solution because FedAuth cookies have HttpOnly flag and thus can’t be used in JavaScript.
In order to use this web service we need to send special SOAP body in HTTP POST request. In order to get example of this SOAP open http://example.com/_vti_bin/authentication.asmx in browser (where instead of http://example.com you should use url of your site). It will show list of available web methods:
Login
Mode
Click on Login and you will get needed SOAP examples (in this article we will use SOAP 1.1):
In this SOAP body we need to set actual username and password in appropriate xml tags.
Here is example which shows how to send this SOAP body to authentication.asmx web service and get authentication cookies:
var cookies = new CookieContainer();
var handler = new HttpClientHandler();
handler.CookieContainer = cookies;
using (var httpClient = new HttpClient(handler))
{
string soapBody =
"<soap:Envelope xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>" +
" <soap:Body>" +
" <Login xmlns='http://schemas.microsoft.com/sharepoint/soap/'>" +
" <username>" + username + "</username>" +
" <password>" + password + "</password>" +
" </Login>" +
" </soap:Body>" +
"</soap:Envelope>";
var req = new HttpRequestMessage(HttpMethod.Post, "http://example.com/_vti_bin/authentication.asmx");
req.Content = new StringContent(soapBody, Encoding.UTF8, "text/xml")
var res = httpClient.SendAsync(req).GetAwaiter().GetResult();
if (res.StatusCode == HttpStatusCode.OK)
{
var response = res.Content.ReadAsStringAsync().Result;
var cookie = cookies.GetCookies(new Uri("http://example.com")).Cast<Cookie>().FirstOrDefault(c => c.Name == "FedAuth");
}
}
In this example we first create HTTP POST request to authentication.asmx using SOAP body retrieved above and then if authentication was successful read FedAuth cookies returned from server. After that we may use these cookies for making authenticated calls to other web services. In one of the future articles I will show how to do that.
Sharepoint Online allows to share your sites with external users. At first administrator should enable external sharing on the tenant (organization) level (see Manage sharing settings). After that you may set external capabilities for each individual site (see Set sharing capabilities of Sharepoint site collection via client object model for how to do that). In provided example SharingCapabilities is enum which has the following values (they are quite self-descriptive so I won’t describe they here):
Disabled
ExternalUserSharingOnly
ExternalUserAndGuestSharing
ExistingExternalUserSharingOnly
In order to read sharing capabilities of existing Sharepoint Online sites via CSOM you may use the following example:
string username = ...;
string password = ...;
var adminContext = new ClientContext("https://{tenant}-admin.sharepoint.com");
var secure = new SecureString();
foreach (char c in password)
{
secure.AppendChar(c);
}
var credentials = new SharePointOnlineCredentials(username, secure);
adminContext.Credentials = credentials;
adminContext.Load(adminContext.Site);
adminContext.ExecuteQuery();
var tenant = new Tenant(adminContext);
var properties = tenant.GetSiteProperties(0, true);
adminContext.Load(properties);
adminContext.ExecuteQuery();
foreach (SiteProperties p in properties)
{
Console.WriteLine(p.Url + ": " + p.SharingCapability);
}
This example outputs all sites with their SharingCapability properties. If you need to get SharingCapability for single specific site add condition on site url to the last loop. Hope it will help you.
Today we faced with scenario when such login name could not be resolved on one customer’s tenant. After research it was found that similar issue was also reported on OfficeDevPnP github project page (see here). So as it turned out on old tenants this way of getting “Everyone except external users” may not work. As workaround you may use the following solution from OfficeDevPnP:
I.e. at first we try to get login name using "c:0-.f|rolemanager|spo-grid-all-users/” + tenantId and try to resolve group with this name. If it fails we call GetEveryoneExceptExternalUsersClaimName() extension method which returns localized name of “Everyone except external users” group for current tenant (it has translations of group name for all supported languages) and tries to resolve this special group using this name. This code will work both on new and old tenants.
In Sharepoint app model we may need to grant permissions to Sharepoint app on AppInv.aspx page by providing appropriate permissions request xml. If permissions are granted on Tenant level you need to open AppInv.aspx in context of Central admin i.e. https://{tenant}-admin.sharepoint.com:
It was historically quite painful to automate this process as automatic permissions grant is not currently possible. There were attempts to automate O365 login and automate trust process using COM automation in PowerShell (using New-Object -com internetexplorer.application): https://github.com/wulfland/ScriptRepository/blob/master/Apps/Apps/Deploy-SPApp.ps1. With this approach script opens AppInv.aspx page and simulates user’s input.
However O365 login experience was changed since this script was implemented and there is no guarantee that it won’t be changed further. Also there may be several login scenarios:
user may be already logged in if chose Remember credentials during previous login
user may use MFA with SMS, authenticator app or something else which will make login automation even more complicated
Keeping that in mind I implemented the following semi-automatic way of granting app permissions and trust the app:
1. app is registered in Azure AD via PowerShell (in Sharepoint Online it is not necessary to register app which will be used for communicating with Sharepoint via AppRegNew.aspx. You may also register it in Azure Portal > App Registrations). See e.g. Create an Azure Active Directory Application and Key using PowerShell for example
2. Then script opens AppInv.aspx page in IE (using Start-Process cmdlet) and asks user to authenticate him/herself manually. After that user returns to the script and clicks Enter – all other steps (grant permissions and trust the app) are performed by the following PowerShell script:
function Trust-SPAddIn {
[CmdletBinding(SupportsShouldProcess=$true)]
[OutputType([int])]
Param
(
[Parameter(Mandatory=$true, Position=0)]
[string]$AppInstanceId,
[Parameter(Mandatory=$true, Position=1)]
[string]$WebUrl,
[parameter(Mandatory=$true, Position=2)]
[string]$UserName,
[parameter(Mandatory=$true, Position=3)]
[string]$Password
)
$ie = New-Object -com internetexplorer.application
try {
Log-Warn ("Script will now open $WebUrl. Please authenticate yourself and wait until Admin Center home page will be loaded.")
Log-Warn ("After that leave Admin Center window opened (don't close it), return to the script and follow provided instructions.")
Log-Warn ("In case you are already signed in Admin Center window will be opened without asking to login. In this case wait until Admin Center window will be loaded, leave it opened and return to the script.")
if (-not $silently) {
Log-Warn ("Press Enter to open $WebUrl...")
Read-Host
}
$ie.Visible = $true
$ie.Navigate2($WebUrl)
if (-not $silently) {
Log-Warn ("Wait until Admin Center window will be fully loaded and press Enter to continue installation")
Log-Warn ("Don't close Admin Center window - script will close it automatically")
Read-Host
}
$authorizeURL = "$($WebUrl.TrimEnd('/'))/_layouts/15/appinv.aspx"
Log-Info ("Open $authorizeURL...")
$ie.Visible = $false
$ie.Navigate2($authorizeURL)
WaitFor-IEReady $ie -initialWaitInSeconds 3
Log-Info ("Grant permissions to the app...")
$appIdInput = $ie.Document.getElementById("ctl00_ctl00_PlaceHolderContentArea_PlaceHolderMain_IdTitleEditableInputFormSection_ctl01_TxtAppId")
$appIdInput.value = $AppInstanceId
$lookupBtn = $ie.Document.getElementById("ctl00_ctl00_PlaceHolderContentArea_PlaceHolderMain_IdTitleEditableInputFormSection_ctl01_BtnLookup")
$lookupBtn.Click()
WaitFor-IEReady $ie -initialWaitInSeconds 3
Log-Info ("Step 1 of 2 done")
$appIdInput = $ie.Document.getElementById("ctl00_ctl00_PlaceHolderContentArea_PlaceHolderMain_TitleDescSection_ctl01_TxtPerm")
$appIdInput.value = '<AppPermissionRequests AllowAppOnlyPolicy="true"><AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="FullControl" /></AppPermissionRequests>'
$createBtn = $ie.Document.getElementById("ctl00_ctl00_PlaceHolderContentArea_PlaceHolderMain_ctl01_RptControls_BtnCreate")
$createBtn.Click()
WaitFor-IEReady $ie -initialWaitInSeconds 3
Log-Info ("Step 2 of 2 done")
Log-Info ("Trust the app...")
$trustBtn = $ie.Document.getElementById("ctl00_ctl00_PlaceHolderContentArea_PlaceHolderMain_BtnAllow")
$trustBtn.Click()
WaitFor-IEReady $ie -initialWaitInSeconds 3
Log-Info ("All steps are done")
}
finally {
$ie.Quit()
}
}
WaitFor-IEReady helper method is given from original script mentioned above so credits go to it’s author:
Log-Info and Log-Warn are basic logger methods and you may implement them as needed for your scenario. Since we delegated login to the end user we don’t need to handle different O365 login scenarios and script is greatly simplified, e.g. there is no need to perform javascript activities which work not very stable via COM automation.
Sharepoint farm administrators are powerful users who may perform administrative actions in Sharepoint farm (see SharePoint Farm Administrator account for more details). Sometime you may need to check whether or not current user is farm admin in order to allow or disallow specific actions. You may do that using SPFarm.CurrentUserIsAdministrator method. The problem however that it will work as expected only in context of Central administration web application (i.e. if page which calls this method is running in Central administration). If you will try to call this method from regular Sharepoint web application it will always return false even if current user is added to Farm administrations group on your farm - you may add users to Farm administrators using Central administration > Manage the farm administrators group:
In this case (when you need to call SPFarm.CurrentUserIsAdministrator from regular Sharepoint web application) you have to use overridden version of this method with boolean parameter:
allowContentApplicationAccess true to make the check work in the content port of a Web application; otherwise, false.
In Sharepoint Online you may assign permissions to all employees of your organization using special group “Everyone except external users”. In order to add permissions to this group programmatically we need to know login name of the appropriate object in Sharepoint object model. In this article I will show how to get login name of this special group programmatically.
The main difficulty is that login name of “Everyone except external users” group is different per tenant. But the good thing is that it is built using known rule:
c:0-.f|rolemanager|spo-grid-all-users/{realm}
where instead of {realm} placeholder you need to use realm for your tenant. We can get realm using TokenHelper.GetRealmFromTargetUrl() method. So code will look like this:
protected virtual string GetEveryoneExceptExternalsLoginName(string siteUrl)
{
var realm = TokenHelper.GetRealmFromTargetUrl(new Uri(siteUrl));
return string.Format("c:0-.f|rolemanager|spo-grid-all-users/{0}", realm);
}
public static string GetRealmFromTargetUrl(Uri targetApplicationUri)
{
#if ONPREMISES
if (targetApplicationUri.Scheme.ToLower() == "https")
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
#endif
WebRequest request = WebRequest.Create(targetApplicationUri + "/_vti_bin/client.svc");
request.Headers.Add("Authorization: Bearer ");
try
{
using (request.GetResponse())
{
}
}
catch (WebException e)
{
if (e.Response == null)
{
return null;
}
string bearerResponseHeader = e.Response.Headers["WWW-Authenticate"];
if (string.IsNullOrEmpty(bearerResponseHeader))
{
return null;
}
const string bearer = "Bearer realm=\"";
int bearerIndex = bearerResponseHeader.IndexOf(bearer, StringComparison.Ordinal);
if (bearerIndex < 0)
{
return null;
}
int realmIndex = bearerIndex + bearer.Length;
if (bearerResponseHeader.Length >= realmIndex + 36)
{
string targetRealm = bearerResponseHeader.Substring(realmIndex, 36);
Guid realmGuid;
if (Guid.TryParse(targetRealm, out realmGuid))
{
return targetRealm;
}
}
}
return null;
}
After that you will be able to grant permissions to “Everyone except external users” programmatically.
When you create new site collection (doesn’t matter modern or classic) using Tenant.CreateSite() method you need to specify primary site collection administrator in SiteCreationProperties.Owner property. So if you would check site collection administrators of the newly created site you would expect to see specified user there. However it is not always the case. Sometimes Sharepoint really shows specified user there:
But sometimes instead of actual user account there will be Company Administrator – special user which covers all users in the directory with Global Administrator rights (see e.g. Special SharePoint Groups):
It happens regardless of specified user directory role: it may have Global Administrator role and may not have it:
And even more – if user have more directory roles there may be both Company Administrator and Sharepoint Service Administrator:
I.e. looks like Sharepoint get’s user’s roles and assigns permissions to these roles instead of actual user account. But again – for some user accounts it just adds actual user account to Site collection administrators. Logic behind this behavior is not yet clear so if you have any thoughts on that please share it in comments.
In Sharepoint you may configure Access request settings which will allow users to ask for access to the site from appropriate responsible person (I intentially don’t tell site owner here because recipient of access requests may be different person – see below). In order to do it first of all you need to configure outgoing email settings in Central administration. Access request settings for web site may be configured from Site settings > Site permissions > Access request settings:
When you click this link Sharepoint loads setrqacc.aspx into modal window. If we will check its codebehind class Microsoft.SharePoint.ApplicationPages.SetRequestAccess we will see that it first checks SPWebApplication.RequestAccessEnabled internal property:
In this property it checks another property IsEmailServerSet:
which in turn checks outgoing email server address:
That’s why setting outgoing email settings should be done before to configure Access request settings.
Now let’s check SetRequestAccess.OnLoad method again and see how “Allow access requests” checkbox is initialized with checked/unckecked values. As it is shown above OnLoad method calls ToggleSelectedAndSetObjectType method:
and the last one checks “Allow access requests” checkbox when SPWeb.RequestAccessEnabled property set to true. Let’s check code of SPWeb.RequestAccessEnabled property:
I.e. it is readonly property which returns true only when Access request email is set, i.e. other property SPWeb.RequestAccessEmail. So basically in order to enable Access requests you need to set SPWeb.RequestAccessEmail for your web site.
The problem however is that Sharepoint for some sites sets SPWeb.RequestAccessEmail to default value someone@example.com – see first picture in this post. There is internal method SPWeb.EnableAccessRequestsIfNeeded where it is done:
Here it’s code as it doesn’t fit to post column’s width:
So for root webs of new site collections which have HasUniqueRoleAssignments = true and which use web template id except 3, 16, 17, 18 (and of course if SPWebApplication.RequestAccessEnabled is set to true – see above) Sharepoint will set SPWeb.RequestAccessEmail to default value someone@example.com which will mean that Access requests will be enabled there by default. In the following forum post Access Requests recipient by Default it is mentioned that in this case, i.e. when default email address someone@example.com is used, SPWeb.Site.Owner.Email will be used for sending access requests, although I didn’t check it by myself.
And one more thing: if you are working with CSOM note that Web.RequestAccessEmail property is available there only starting with version 16.1.4727.1200 onwards. Basically it means that it is not possible to change Access requests settings by CSOM for Sharepoint 2013 on-premise. It is still possible though via basic server object model.
In addition to changing anonymous settings for the Sharepoint sites (see AllowAnonymousAccess, AnonymousState and AnonymousPermMask64 properties for Sharepoint sites with different anonymous configurations) it may be needed to enable anonymous access for particular lists and doclibs. In this case you need to set Anonymous users can access: Lists and libraries for the parent web (see above link) and enable anonymous access for the list/doclib. It is done from List settings > Permissions for this document library > Anonymous Access. Here you may check only View items permissions for anonymous users on regular web sites with read anonymous access. Let’s see how SPList.AnonymousPermMask64 property will be changed.
1. No anonymous access
In this case SPList.AnonymousPermMask64 = EmptyMask
2. View Items
Now SPList.AnonymousPermMask64 = ViewListItems, OpenItems, ViewVersions, ViewFormPages, Open, UseClientIntegration.
In Sharepoint it is possible to use several anonymous configurations for the sites. They may be changed from Site settings > Site permissions > Anonymous access:
Entire Web site
Lists and libraries
Nothing
Here what description says about them:
Specify what parts of your Web site (if any) anonymous users can access. If you select Entire Web site, anonymous users will be able to view all pages in your Web site and view all lists and items which inherit permissions from the Web site. If you select Lists and libraries, anonymous users will be able to view and change items only for those lists and libraries that have enabled permissions for anonymous users.
And Nothing means that site doesn’t have anonymous access. Depending on used setting Sharepoint changes 3 properties of corresponding SPWeb object: AllowAnonymousAccess, AnonymousState and AnonymousPermMask64. Let’s see how they are changed depending on selection:
Sliding session allows user to use site without being reauthenticated if last action was done less than configured session lifetime. In Sharepoint 2013 FBA the following parameters of security token service config are used for setting session lifetime:
CookieLifetime
FormsTokenLifeTime
LogonTokenCacheExpirationWindow
They are well described in the following article SharePoint 2013 authentication lifetime settings and I won’t repeat it here. The problem is that when you use persistent cookies (i.e. those which are stored on client’s side) only CookieLifetime are actually used (to be more precise, FormsTokenLifeTime is used for setting initial ValidTo value for session security token). In addition to that sliding sessions doesn’t work by default, i.e. regardless of whether user made actions on the site or not he will be logged out after cookies will be expired. Persistent cookies can be set e.g. if user checked “Remember Me” checkbox on the login page:
Here on lines 12-16 code checks whether rememberMe parameter is true and if yes uses persistent cookies.
So is it possible to have sliding expiration sessions when persistent cookies are used? The answer is yes, but in order to do that we will need custom HTTP module which will renew token on each request:
In the module we subscribe on SessionAuthenticationModule.SessionSecurityTokenReceived event (lines 5-6) and in event handler we renew token with extended ValidFrom and ValidTo properties (lines 36-42) which are set from CookieLifetime property of security token service config (lines 29-34) so you may continue configure it from PowerShell.
Then we need to install this module by adding dll to the GAC and the following line to the web.config <modules> section: