I've been playing around with both Azure and F# lately and thought that I would share my knowledge about programming the Azure Active Directory using F#. It took some time to find some good example code, but after some searching I did find this project: https://github.com/AzureADSamples/ConsoleApp-GraphAPI-DotNet. The project shows most of the basic functionality, but I thought I would show it in F# and at the same time try to make it easier for others to understand. So let's started.
TL;DR;
I've put the code on github, https://github.com/mastoj/ADTutorial, so you can download it easier. This will get you started quite easy and manipulating Active Directory in no time at all. When working with this short post/tutorial I had some issues with connecting to the AD's to start with, but the problem just went a way. My guess is that it might have taken some time from the creation of the Active Directory and when it was accessible by code, but that's just a guess.
Creating the Active Directory in Azure
We will create a new directory that we can play with. So to create the directory choose "CUSTOM CREATE" that is available after following the onscreen menu in the picture below.
Enter some information in the "Add directory" (sample data below).
In the active directory list click the arrow for your new directory to get to the details of this directory.
To connect to the Active Directory, which are going to do through a console application, we need to add an application to the Active Directory. So go to the "Application" tab and then click "ADD" at the bottom of the screen. In the dialogue choose "Add an application my organization is developing" and then "WEB APPLICATION AND/OR WEB API", as "REDIRECT URI" and just choose a unique for "APP ID URI" use "http://localhost".
Collecting and configuring the data needed to connect
For the things we are going to do later there are some data we need to collect to be able to connect to the active directory. The things we need are:
- Client Id
- Client secret
- Tenant name
- Tenant id
I had problem finding this information, so I'll try to make it as clear as I can. Before we locate those for things there is also some other configuration settings which is more static.
- Resource url (url to the resource we acces to configure AD): https://graph.windows.net
- Authentication string: https://login.windows.net/
The last thing we need to do is configure the permissions of the application so we can access the active directory.
The client id
The client id is found under the "CONFIGURE" tab.
The client secret
This wasn't as straightforward as the id, but with secret they often mean "key". So on the "CONFIGURE" tab we need to generate a 1 or 2 year key that we can use. The key will only be visible after you save and only that time.
The tenant name and id
The name is the easiest part, if you haven't got any custom domain names this is the domain name you used for the "Active Directory". So in this example it is "fsharptest.onmicrosoft.com" (no http before the name).
The id is a little bit harder to find. There are probably many ways to find the id, one is to visit the url: https://login.windows.net/fsharptest.onmicrosoft.com/FederationMetadata/2007-06/FederationMetadata.xml, but change my domain with yours. The tenant id is the guid in the "entityId" url on the first node in the document.
Another way to find the tenant id is to expand the "ENABLE USERS TO SIGN ON" and there it is in the magic url:
Time to code
Since I do prefer F# when I can the coding part will be done in F#, but you can easily translate the code to C# if you want to. I'll write everything in one file in a console app. To get started the easiest way is to use to nuget packages:
Microsoft.IdentityModel.Clients.ActiveDirectory
(I used version 2.13.112191810)Microsoft.Azure.ActiveDirectory.GraphClient
(I used version 2.0.3)
The first part of the files are the settings, I like to keep those in a separate module:
[<AutoOpen>]
module AdSettings =
let clientId = "the client id guid"
let clientSecret = "the key from azure"
let tenantId = "the tenant id"
let tenantName = "the domain"
let resourceUrl = "https://graph.windows.net"
let authString = "https://login.windows.net/" + tenantName
Then we have the actual client code, the code is not intended for production use I'm just showing of the AD integration.
[<AutoOpen>]
module AdClient =
open Microsoft.Azure.ActiveDirectory.GraphClient
open Microsoft.IdentityModel.Clients.ActiveDirectory
open System
open System.Linq.Expressions
open System.Threading.Tasks
open Microsoft.FSharp.Linq.RuntimeHelpers
let getAuthenticationToken (clientId:string) (clientSecret:string) tenantName =
let authenticationContext = AuthenticationContext(authString, false)
let credentials = ClientCredential(clientId, clientSecret)
let authenticationResult = authenticationContext.AcquireToken(resourceUrl, credentials)
authenticationResult.AccessToken
let activeDirectoryClient (tenantId:string) token =
let serviceRoot = Uri(resourceUrl + "/" + tenantId)
let tokenTask = Func<Task<string>>(fun() ->Task.Factory.StartNew<string>(fun() -> token))
let activeDirectoryClient = ActiveDirectoryClient(serviceRoot, tokenTask)
activeDirectoryClient
let client = getAuthenticationToken clientId clientSecret tenantName |> activeDirectoryClient tenantId
let toExpression<'a> quotationExpression = quotationExpression |> LeafExpressionConverter.QuotationToExpression |> unbox<Expression<'a>>
let getGroup groupName =
let matchExpression = <@Func<IGroup,bool>(fun (group:IGroup) -> group.DisplayName = groupName) @>
let filter = toExpression<Func<IGroup,bool>> matchExpression
let groups = client
.Groups
.Where(filter)
.ExecuteAsync()
.Result
.CurrentPage
|> List.ofSeq
match groups with
| [] -> None
| x::[] -> Some (x :?> Group)
| _ -> raise (Exception("more than one group exists"))
let addGroup groupName =
match getGroup groupName with
| None ->
let group = Group()
group.DisplayName <- groupName
group.Description <- groupName
group.MailNickname <- groupName
group.MailEnabled <- Nullable(false)
group.SecurityEnabled <- Nullable(true)
client.Groups.AddGroupAsync(group).Wait()
Some group
| Some x -> Some (x :?> Group)
let getUser userName =
let matchExpression = <@Func<IUser,bool>(fun (user:IUser) -> user.DisplayName = userName) @>
let filter = toExpression<Func<IUser,bool>> matchExpression
let users = client
.Users
.Where(filter)
.ExecuteAsync()
.Result
.CurrentPage
|> List.ofSeq
match users with
| [] -> None
| x::[] -> Some x
| _ -> raise (Exception("more than one user exists with that name"))
let addUser userName =
match getUser userName with
| None ->
let passwordProfile() =
let passwd = PasswordProfile()
passwd.ForceChangePasswordNextLogin <- Nullable(true)
passwd.Password <- "Ch@ng3NoW!"
passwd
let user = User()
user.PasswordProfile <- passwordProfile()
user.DisplayName <- userName
user.UserPrincipalName <- userName + "@fsharptest.onmicrosoft.com"
user.AccountEnabled <- Nullable(true)
user.MailNickname <- userName
client.Users.AddUserAsync(user).Wait()
Some user
| Some x -> Some (x :?> User)
let getMembers (group:Group) =
let groupFetcher = (group :> IGroupFetcher)
let members = groupFetcher.Members.ExecuteAsync().Result
members.CurrentPage
let groupContainsUser (group:Group) (user:User) =
group |> getMembers |> Seq.map (fun o -> (o :?> User).DisplayName) |> Seq.exists (fun s -> s = user.DisplayName)
let addUserToGroup (group:Group) (user:User) =
match groupContainsUser (group:Group) (user:User) with
| false ->
group.Members.Add(user)
group.UpdateAsync().Wait()
group
| true ->
group
The client code is sort of straightforward. The first part, down to the client
declaration is just about connecting to the API. The names of the functions after that explains them self. To do a query we need to create an expression which we do by creating a code quotation which we translate to a Func
. The code here is not by any means what you should have in production, but it show you some part of the API and what you can do.
The last part is a small program that uses the module we just created:
open System
open Microsoft.Azure.ActiveDirectory.GraphClient
[<EntryPoint>]
let main argv =
try
let group = addGroup "newgroup" |> Option.get
let user = addUser "charlie" |> Option.get
user |> addUserToGroup group |> ignore
let group2 = getGroup "newgroup" |> Option.get
printfn "Group name: %s" group2.DisplayName
let membersOfGroup = getMembers group
let members = membersOfGroup |> Seq.map (fun o -> (o :?> User).DisplayName) |> String.concat ", "
printfn "Members: %s" members
Console.ReadLine() |> ignore
with
| :? Exception as ex -> printfn "%s" ex.Message
0